From 49f51ca76462917bd11b92dbbc3e8c1d248ca88f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 14 Mar 2024 11:49:56 -0700 Subject: [PATCH 001/184] Initial implementation of Object --- packages/compiler/src/core/checker.ts | 82 ++++++++++++++++++- packages/compiler/src/core/messages.ts | 13 +++ packages/compiler/src/core/parser.ts | 81 +++++++++++++++++- packages/compiler/src/core/scanner.ts | 4 +- packages/compiler/src/core/semantic-walker.ts | 5 +- packages/compiler/src/core/types.ts | 46 ++++++++++- .../compiler/src/formatter/print/printer.ts | 56 +++++++++++++ .../compiler/src/server/type-signature.ts | 2 + packages/compiler/test/parser.test.ts | 9 ++ .../tspd/src/ref-doc/utils/type-signature.ts | 2 + 10 files changed, 293 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 4e4e49ca8e..3a2ad54962 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -86,6 +86,10 @@ import { NodeFlags, NumericLiteral, NumericLiteralNode, + ObjectLiteral, + ObjectLiteralNode, + ObjectLiteralProperty, + ObjectLiteralPropertyNode, Operation, OperationStatementNode, Projection, @@ -659,6 +663,8 @@ export function createChecker(program: Program): Checker { return checkNamespace(node); case SyntaxKind.OperationStatement: return checkOperation(node, mapper); + case SyntaxKind.ObjectLiteral: + return checkObjectLiteral(node, mapper); case SyntaxKind.NumericLiteral: return checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: @@ -2948,6 +2954,79 @@ export function createChecker(program: Program): Checker { } } + function checkObjectLiteral(node: ObjectLiteralNode, mapper: TypeMapper | undefined) { + const type: ObjectLiteral = createType({ + kind: "Object", + node: node, + properties: checkObjectLiteralProperties(node, mapper), + indexer: undefined, + decorators: [], + derivedModels: [], + }); + return finishType(type); + } + + function checkObjectLiteralProperties(node: ObjectLiteralNode, mapper: TypeMapper | undefined) { + const properties = createRekeyableMap(); + + for (const prop of node.properties!) { + if ("id" in prop) { + const newProp = checkObjectLiteralProperty(prop, mapper); + if (newProp) { + properties.set(newProp.name, newProp); + } + } else { + const newProperties = checkObjectSpreadProperty(prop.target, mapper); + for (const newProp of newProperties) { + properties.set(newProp.name, newProp); + } + } + } + return properties; + } + + function checkObjectLiteralProperty( + node: ObjectLiteralPropertyNode, + mapper: TypeMapper | undefined + ): ObjectLiteralProperty | undefined { + const type = getTypeForNode(node.value, mapper); + if ( + type.kind !== "Object" && + type.kind !== "String" && + type.kind !== "Number" && + type.kind !== "Boolean" + ) { + reportCheckerDiagnostic(createDiagnostic({ code: "not-literal", target: node.value })); + return undefined; + } + + return createAndFinishType({ + kind: "ObjectProperty", + node, + name: node.id.sv, + type: type as any, + }); + } + + function checkObjectSpreadProperty( + targetNode: TypeReferenceNode, + mapper: TypeMapper | undefined + ): ObjectLiteralProperty[] { + const targetType = getTypeForNode(targetNode, mapper); + + if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) { + return []; + } + // TODO: instanceof is because of conflict of the Object type from projection. + if (targetType.kind !== "Object" || !(targetType.properties instanceof Map)) { + reportCheckerDiagnostic(createDiagnostic({ code: "spread-object", target: targetNode })); + return []; + } + + // TODO: do we want to clone or use the exact same reference here? + return [...targetType.properties.values()].map((prop) => cloneType(prop)); + } + function createUnion(options: Type[]): Union { const variants = createRekeyableMap(); const union: Union = createAndFinishType({ @@ -5190,7 +5269,8 @@ export function createChecker(program: Program): Checker { switch (base.kind) { case "Object": - return base.properties[member] || errorType; + // TODO: resolve conflict here + return (base.properties as any)[member] || errorType; default: const typeOps = projectionMembers[base.kind]; if (!typeOps) { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index b7311898ed..896da90dd7 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -391,12 +391,25 @@ const diagnostics = { selfSpread: "Cannot spread type within its own declaration.", }, }, + "unsupported-default": { severity: "error", messages: { default: paramMessage`Default must be have a value type but has type '${"type"}'.`, }, }, + "spread-object": { + severity: "error", + messages: { + default: "Cannot spread properties of non-object type.", + }, + }, + "not-literal": { + severity: "error", + messages: { + default: "Type must be a literal type.", + }, + }, unassignable: { severity: "error", messages: { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index e9da33d870..f604b7999b 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -62,6 +62,9 @@ import { Node, NodeFlags, NumericLiteralNode, + ObjectLiteralNode, + ObjectLiteralPropertyNode, + ObjectLiteralSpreadPropertyNode, OperationSignature, OperationStatementNode, ParseOptions, @@ -124,7 +127,12 @@ type ParseListItem = K extends UnannotatedListKind ? () => T : (pos: number, decorators: DecoratorExpressionNode[]) => T; -type OpenToken = Token.OpenBrace | Token.OpenParen | Token.OpenBracket | Token.LessThan; +type OpenToken = + | Token.OpenBrace + | Token.OpenParen + | Token.OpenBracket + | Token.LessThan + | Token.HashBrace; type CloseToken = Token.CloseBrace | Token.CloseParen | Token.CloseBracket | Token.GreaterThan; type DelimiterToken = Token.Comma | Token.Semicolon; @@ -186,6 +194,14 @@ namespace ListKind { toleratedDelimiter: Token.Comma, } as const; + export const ObjectLiteralProperties = { + ...PropertiesBase, + open: Token.HashBrace, + close: Token.CloseBrace, + delimiter: Token.Comma, + toleratedDelimiter: Token.Comma, + } as const; + export const InterfaceMembers = { ...PropertiesBase, open: Token.OpenBrace, @@ -954,6 +970,46 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseObjectLiteralPropertyOrSpread( + pos: number, + decorators: DecoratorExpressionNode[] + ): ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode { + reportInvalidDecorators(decorators, "object literal property"); + + return token() === Token.Ellipsis + ? parseObjectLiteralSpreadProperty(pos) + : parseObjectLiteralProperty(pos); + } + + function parseObjectLiteralSpreadProperty(pos: number): ObjectLiteralSpreadPropertyNode { + parseExpected(Token.Ellipsis); + + // This could be broadened to allow any type expression + const target = parseReferenceExpression(); + + return { + kind: SyntaxKind.ObjectLiteralSpreadProperty, + target, + ...finishNode(pos), + }; + } + + function parseObjectLiteralProperty(pos: number): ObjectLiteralPropertyNode { + const id = parseIdentifier({ + message: "property", + }); + + parseExpected(Token.Colon); + const value = parseExpression() as any; // TODO? only parse object expressions or let checker verify that? + + return { + kind: SyntaxKind.ObjectLiteralProperty, + id, + value, + ...finishNode(pos), + }; + } + function parseScalarStatement( pos: number, decorators: DecoratorExpressionNode[] @@ -1415,6 +1471,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const directives = parseDirectiveList(); reportInvalidDirective(directives, "expression"); continue; + case Token.HashBrace: + return parseObjectLiteral(); case Token.VoidKeyword: return parseVoidKeyword(); case Token.NeverKeyword: @@ -1491,6 +1549,19 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseObjectLiteral(): ObjectLiteralNode { + const pos = tokenPos(); + const properties = parseList( + ListKind.ObjectLiteralProperties, + parseObjectLiteralPropertyOrSpread + ); + return { + kind: SyntaxKind.ObjectLiteral, + properties, + ...finishNode(pos), + }; + } + function parseStringLiteral(): StringLiteralNode { const pos = tokenPos(); const value = tokenValue(); @@ -3220,6 +3291,7 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined ); case SyntaxKind.ModelSpreadProperty: return visitNode(cb, node.target); + case SyntaxKind.ModelStatement: return ( visitEach(cb, node.decorators) || @@ -3362,7 +3434,12 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined return visitNode(cb, node.head) || visitEach(cb, node.spans); case SyntaxKind.StringTemplateSpan: return visitNode(cb, node.expression) || visitNode(cb, node.literal); - + case SyntaxKind.ObjectLiteral: + return visitEach(cb, node.properties); + case SyntaxKind.ObjectLiteralProperty: + return visitNode(cb, node.id) || visitNode(cb, node.value); + case SyntaxKind.ObjectLiteralSpreadProperty: + return visitNode(cb, node.target); // no children for the rest of these. case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 75dd01ab43..7e589251cb 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -84,6 +84,7 @@ export enum Token { At, AtAt, Hash, + HashBrace, Star, ForwardSlash, Plus, @@ -213,6 +214,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.At, "'@'"], [Token.AtAt, "'@@'"], [Token.Hash, "'#'"], + [Token.HashBrace, "'#{'"], [Token.Star, "'*'"], [Token.ForwardSlash, "'/'"], [Token.Plus, "'+'"], @@ -511,7 +513,7 @@ export function createScanner( return lookAhead(1) === CharCode.At ? next(Token.AtAt, 2) : next(Token.At); case CharCode.Hash: - return next(Token.Hash); + return lookAhead(1) === CharCode.OpenBrace ? next(Token.HashBrace, 2) : next(Token.Hash); case CharCode.Plus: return isDigit(lookAhead(1)) ? scanSignedNumber() : next(Token.Plus); diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index d6d4a5657c..74022567bb 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -95,7 +95,7 @@ export function scopeNavigationToNamespace( return ListenerFlow.NoRecursion; } } - return callback(x as any); + return (callback as any)(x as any); }; } return wrappedListeners as any; @@ -143,7 +143,7 @@ function createNavigationContext( ): NavigationContext { return { visited: new Set(), - emit: (key, ...args) => listeners[key]?.(...(args as [any])), + emit: (key, ...args) => (listeners as any)[key]?.(...(args as [any])), options: computeOptions(options), }; } @@ -392,6 +392,7 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { case "Intrinsic": case "Number": case "String": + case "ObjectProperty": return; default: // Dummy const to ensure we handle all types. diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 8dd2b8e58b..45716fd255 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -109,6 +109,8 @@ export type Type = | Decorator | FunctionParameter | ObjectType + | ObjectLiteral + | ObjectLiteralProperty | Projection; export type StdTypes = { @@ -289,6 +291,18 @@ export interface ModelProperty extends BaseType, DecoratedType { model?: Model; } +export interface ObjectLiteral extends BaseType { + kind: "Object"; + properties: RekeyableMap; +} + +export interface ObjectLiteralProperty extends BaseType { + kind: "ObjectProperty"; + name: string; + node: ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode; + type: LiteralType | ObjectLiteral; +} + export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Scalar"; name: string; @@ -817,6 +831,9 @@ export enum SyntaxKind { Return, JsNamespaceDeclaration, TemplateArgument, + ObjectLiteral, + ObjectLiteralProperty, + ObjectLiteralSpreadProperty, } export const enum NodeFlags { @@ -911,7 +928,10 @@ export type Node = | ProjectionModelPropertyNode | ProjectionModelSpreadPropertyNode | ProjectionStatementNode - | ProjectionNode; + | ProjectionNode + | ObjectLiteralNode + | ObjectLiteralPropertyNode + | ObjectLiteralSpreadPropertyNode; /** * Node that can be used as template @@ -1067,6 +1087,7 @@ export type Expression = | ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode + | ObjectLiteralNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode @@ -1257,6 +1278,29 @@ export interface ModelSpreadPropertyNode extends BaseNode { readonly parent?: ModelStatementNode | ModelExpressionNode; } +export interface ObjectLiteralNode extends BaseNode { + readonly kind: SyntaxKind.ObjectLiteral; + readonly properties: (ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode)[]; +} + +export interface ObjectLiteralPropertyNode extends BaseNode { + readonly kind: SyntaxKind.ObjectLiteralProperty; + readonly id: IdentifierNode; + readonly value: + | StringLiteralNode + | NumericLiteralNode + | BooleanLiteralNode + | ObjectLiteralNode + | TupleExpressionNode; + readonly parent?: ObjectLiteralNode; +} + +export interface ObjectLiteralSpreadPropertyNode extends BaseNode { + readonly kind: SyntaxKind.ObjectLiteralSpreadProperty; + readonly target: TypeReferenceNode; + readonly parent?: ObjectLiteralNode; +} + export type LiteralNode = | StringLiteralNode | NumericLiteralNode diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index dd62367aaa..92232c1447 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -31,6 +31,9 @@ import { Node, NodeFlags, NumericLiteralNode, + ObjectLiteralNode, + ObjectLiteralPropertyNode, + ObjectLiteralSpreadPropertyNode, OperationSignatureDeclarationNode, OperationSignatureReferenceNode, OperationStatementNode, @@ -366,6 +369,16 @@ export function printNode( options, print ); + case SyntaxKind.ObjectLiteral: + return printObjectLiteral(path as AstPath, options, print); + case SyntaxKind.ObjectLiteralProperty: + return printObjectLiteralProperty(path as AstPath, options, print); + case SyntaxKind.ObjectLiteralSpreadProperty: + return printObjectLiteralSpreadProperty( + path as AstPath, + options, + print + ); case SyntaxKind.StringTemplateSpan: case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: @@ -963,6 +976,45 @@ export function printModelExpression( } } +export function printObjectLiteral( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + const node = path.node; + const hasProperties = node.properties && node.properties.length > 0; + const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling); + if (!hasProperties && !nodeHasComments) { + return "{}"; + } + const lineDoc = softline; + const body: Doc[] = [ + joinMembersInBlock(path, "properties", options, print, ifBreak(",", ", "), softline), + ]; + if (nodeHasComments) { + body.push(printDanglingComments(path, options, { sameIndent: true })); + } + return group(["{", ifBreak("", " "), indent(body), lineDoc, ifBreak("", " "), "}"]); +} + +export function printObjectLiteralProperty( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + const node = path.node; + const id = printIdentifier(node.id, options); + return [printDirectives(path, options, print), id, ": ", path.call(print, "value")]; +} + +export function printObjectLiteralSpreadProperty( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + return [printDirectives(path, options, print), "...", path.call(print, "target")]; +} + export function printModelStatement( path: AstPath, options: TypeSpecPrettierOptions, @@ -1078,6 +1130,8 @@ function shouldWrapMemberInNewLines( | UnionVariantNode | ProjectionModelPropertyNode | ProjectionModelSpreadPropertyNode + | ObjectLiteralPropertyNode + | ObjectLiteralSpreadPropertyNode >, options: any ): boolean { @@ -1086,6 +1140,8 @@ function shouldWrapMemberInNewLines( (node.kind !== SyntaxKind.ModelSpreadProperty && node.kind !== SyntaxKind.ProjectionModelSpreadProperty && node.kind !== SyntaxKind.EnumSpreadMember && + node.kind !== SyntaxKind.ObjectLiteralProperty && + node.kind !== SyntaxKind.ObjectLiteralSpreadProperty && shouldDecoratorBreakLine(path as any, options, { tryInline: DecoratorsTryInline.modelProperty, })) || diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index a2625b3979..f7d7e1326f 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -77,6 +77,8 @@ function getTypeSignature(type: Type | ValueType): string { return "(projection)"; case "Object": return "(object)"; + case "ObjectProperty": + return "(object property)"; default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index c202858733..a209a9ff59 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -223,6 +223,15 @@ describe("compiler: parser", () => { parseErrorEach([['union A { @myDec "x" x: number, y: string }', [/';' expected/]]]); }); + describe("object literals", () => { + parseEach([ + `alias A = #{a: "abc"};`, + `alias A = #{a: "abc", b: "def"};`, + `alias A = #{a: "abc", ...B};`, + `alias A = #{a: "abc", ...B, c: "ghi"};`, + ]); + }); + describe("valueof expressions", () => { parseEach([ "alias A = valueof string;", diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 15b56f8d96..9df9ab482f 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -67,6 +67,8 @@ export function getTypeSignature(type: Type | ValueType): string { return "(projection)"; case "Object": return "(object)"; + case "ObjectProperty": + return "(object property)"; default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); From a517a063318e9c1eab3966e00ef743acbaf2ee3a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 14 Mar 2024 13:31:16 -0700 Subject: [PATCH 002/184] Add support for tuple literals --- packages/compiler/src/core/checker.ts | 58 ++++++++++++++++--- packages/compiler/src/core/parser.ts | 25 +++++++- packages/compiler/src/core/scanner.ts | 12 +++- packages/compiler/src/core/semantic-walker.ts | 1 + packages/compiler/src/core/types.ts | 25 +++++--- .../compiler/src/formatter/print/printer.ts | 21 +++++++ .../compiler/src/server/type-signature.ts | 4 +- packages/compiler/test/parser.test.ts | 8 +++ .../tspd/src/ref-doc/utils/type-signature.ts | 4 +- 9 files changed, 137 insertions(+), 21 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3a2ad54962..3b5b0fbd78 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,5 +1,12 @@ import { $docFromComment, getIndexer, isArrayModelType } from "../lib/decorators.js"; -import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; +import { + MultiKeyMap, + Mutable, + createRekeyableMap, + isArray, + isDefined, + mutate, +} from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; @@ -140,6 +147,8 @@ import { TemplatedType, Tuple, TupleExpressionNode, + TupleLiteral, + TupleLiteralNode, Type, TypeInstantiationMap, TypeMapper, @@ -665,6 +674,8 @@ export function createChecker(program: Program): Checker { return checkOperation(node, mapper); case SyntaxKind.ObjectLiteral: return checkObjectLiteral(node, mapper); + case SyntaxKind.TupleLiteral: + return checkTupleLiteral(node, mapper); case SyntaxKind.NumericLiteral: return checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: @@ -2985,18 +2996,29 @@ export function createChecker(program: Program): Checker { return properties; } - function checkObjectLiteralProperty( - node: ObjectLiteralPropertyNode, - mapper: TypeMapper | undefined - ): ObjectLiteralProperty | undefined { - const type = getTypeForNode(node.value, mapper); + function checkIsLiteralType( + type: Type, + diagnosticTarget: DiagnosticTarget + ): type is LiteralType | ObjectLiteral | TupleLiteral { if ( type.kind !== "Object" && + type.kind !== "TupleLiteral" && type.kind !== "String" && type.kind !== "Number" && type.kind !== "Boolean" ) { - reportCheckerDiagnostic(createDiagnostic({ code: "not-literal", target: node.value })); + reportCheckerDiagnostic(createDiagnostic({ code: "not-literal", target: diagnosticTarget })); + return false; + } + return true; + } + + function checkObjectLiteralProperty( + node: ObjectLiteralPropertyNode, + mapper: TypeMapper | undefined + ): ObjectLiteralProperty | undefined { + const type = getTypeForNode(node.value, mapper); + if (!checkIsLiteralType(type, node.value)) { return undefined; } @@ -3018,13 +3040,31 @@ export function createChecker(program: Program): Checker { return []; } // TODO: instanceof is because of conflict of the Object type from projection. - if (targetType.kind !== "Object" || !(targetType.properties instanceof Map)) { + if (targetType.kind !== "Object" || !("values" in targetType.properties)) { reportCheckerDiagnostic(createDiagnostic({ code: "spread-object", target: targetNode })); return []; } // TODO: do we want to clone or use the exact same reference here? - return [...targetType.properties.values()].map((prop) => cloneType(prop)); + return [...(targetType.properties as any).values()].map((prop) => cloneType(prop)); + } + + function checkTupleLiteral(node: TupleLiteralNode, mapper: TypeMapper | undefined): TupleLiteral { + const values = node.values + .map((itemNode) => { + const type = getTypeForNode(itemNode, mapper); + if (checkIsLiteralType(type, itemNode)) { + return type; + } else { + return undefined; // TODO: do we want to omit this or include an error type? + } + }) + .filter(isDefined); + return createAndFinishType({ + kind: "TupleLiteral", + node: node, + values, + }); } function createUnion(options: Type[]): Union { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index f604b7999b..646e03419c 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -105,6 +105,7 @@ import { TemplateParameterDeclarationNode, TextRange, TupleExpressionNode, + TupleLiteralNode, TypeReferenceNode, TypeSpecScriptNode, UnionStatementNode, @@ -132,7 +133,8 @@ type OpenToken = | Token.OpenParen | Token.OpenBracket | Token.LessThan - | Token.HashBrace; + | Token.HashBrace + | Token.HashBracket; type CloseToken = Token.CloseBrace | Token.CloseParen | Token.CloseBracket | Token.GreaterThan; type DelimiterToken = Token.Comma | Token.Semicolon; @@ -268,6 +270,13 @@ namespace ListKind { close: Token.CloseBracket, } as const; + export const TupleLiteral = { + ...ExpresionsBase, + allowEmpty: true, + open: Token.HashBracket, + close: Token.CloseBracket, + } as const; + export const FunctionParameters = { ...ExpresionsBase, allowEmpty: true, @@ -1473,6 +1482,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa continue; case Token.HashBrace: return parseObjectLiteral(); + case Token.HashBracket: + return parseTupleLiteral(); case Token.VoidKeyword: return parseVoidKeyword(); case Token.NeverKeyword: @@ -1562,6 +1573,16 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseTupleLiteral(): TupleLiteralNode { + const pos = tokenPos(); + const values = parseList(ListKind.TupleLiteral, parseExpression); + return { + kind: SyntaxKind.TupleLiteral, + values, + ...finishNode(pos), + }; + } + function parseStringLiteral(): StringLiteralNode { const pos = tokenPos(); const value = tokenValue(); @@ -3440,6 +3461,8 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined return visitNode(cb, node.id) || visitNode(cb, node.value); case SyntaxKind.ObjectLiteralSpreadProperty: return visitNode(cb, node.target); + case SyntaxKind.TupleLiteral: + return visitEach(cb, node.values); // no children for the rest of these. case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 7e589251cb..d041e64cb0 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -85,6 +85,7 @@ export enum Token { AtAt, Hash, HashBrace, + HashBracket, Star, ForwardSlash, Plus, @@ -215,6 +216,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.AtAt, "'@@'"], [Token.Hash, "'#'"], [Token.HashBrace, "'#{'"], + [Token.HashBracket, "'#['"], [Token.Star, "'*'"], [Token.ForwardSlash, "'/'"], [Token.Plus, "'+'"], @@ -513,7 +515,15 @@ export function createScanner( return lookAhead(1) === CharCode.At ? next(Token.AtAt, 2) : next(Token.At); case CharCode.Hash: - return lookAhead(1) === CharCode.OpenBrace ? next(Token.HashBrace, 2) : next(Token.Hash); + const ahead = lookAhead(1); + switch (ahead) { + case CharCode.OpenBrace: + return next(Token.HashBrace, 2); + case CharCode.OpenBracket: + return next(Token.HashBracket, 2); + default: + return next(Token.Hash); + } case CharCode.Plus: return isDigit(lookAhead(1)) ? scanSignedNumber() : next(Token.Plus); diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 74022567bb..c65417b702 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -384,6 +384,7 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { case "Decorator": return navigateDecoratorDeclaration(type, context); case "Object": + case "TupleLiteral": case "Projection": case "Function": case "FunctionParameter": diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 45716fd255..5522b5b67e 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -111,6 +111,7 @@ export type Type = | ObjectType | ObjectLiteral | ObjectLiteralProperty + | TupleLiteral | Projection; export type StdTypes = { @@ -300,7 +301,12 @@ export interface ObjectLiteralProperty extends BaseType { kind: "ObjectProperty"; name: string; node: ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode; - type: LiteralType | ObjectLiteral; + type: LiteralType | ObjectLiteral | TupleLiteral; +} + +export interface TupleLiteral extends BaseType { + kind: "TupleLiteral"; + values: (LiteralType | ObjectLiteral | TupleLiteral)[]; } export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { @@ -834,6 +840,7 @@ export enum SyntaxKind { ObjectLiteral, ObjectLiteralProperty, ObjectLiteralSpreadProperty, + TupleLiteral, } export const enum NodeFlags { @@ -931,7 +938,8 @@ export type Node = | ProjectionNode | ObjectLiteralNode | ObjectLiteralPropertyNode - | ObjectLiteralSpreadPropertyNode; + | ObjectLiteralSpreadPropertyNode + | TupleLiteralNode; /** * Node that can be used as template @@ -1088,6 +1096,7 @@ export type Expression = | MemberExpressionNode | ModelExpressionNode | ObjectLiteralNode + | TupleLiteralNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode @@ -1286,12 +1295,7 @@ export interface ObjectLiteralNode extends BaseNode { export interface ObjectLiteralPropertyNode extends BaseNode { readonly kind: SyntaxKind.ObjectLiteralProperty; readonly id: IdentifierNode; - readonly value: - | StringLiteralNode - | NumericLiteralNode - | BooleanLiteralNode - | ObjectLiteralNode - | TupleExpressionNode; + readonly value: Expression; readonly parent?: ObjectLiteralNode; } @@ -1301,6 +1305,11 @@ export interface ObjectLiteralSpreadPropertyNode extends BaseNode { readonly parent?: ObjectLiteralNode; } +export interface TupleLiteralNode extends BaseNode { + readonly kind: SyntaxKind.TupleLiteral; + readonly values: readonly Expression[]; +} + export type LiteralNode = | StringLiteralNode | NumericLiteralNode diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 92232c1447..117a0ee3d7 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -68,6 +68,7 @@ import { TemplateParameterDeclarationNode, TextRange, TupleExpressionNode, + TupleLiteralNode, TypeReferenceNode, TypeSpecScriptNode, UnionExpressionNode, @@ -379,6 +380,8 @@ export function printNode( options, print ); + case SyntaxKind.TupleLiteral: + return printTupleLiteral(path as AstPath, options, print); case SyntaxKind.StringTemplateSpan: case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: @@ -1015,6 +1018,24 @@ export function printObjectLiteralSpreadProperty( return [printDirectives(path, options, print), "...", path.call(print, "target")]; } +export function printTupleLiteral( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + return group([ + "[", + indent( + join( + ", ", + path.map((arg) => [softline, print(arg)], "values") + ) + ), + softline, + "]", + ]); +} + export function printModelStatement( path: AstPath, options: TypeSpecPrettierOptions, diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index f7d7e1326f..6351e10969 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -76,9 +76,11 @@ function getTypeSignature(type: Type | ValueType): string { case "Projection": return "(projection)"; case "Object": - return "(object)"; + return fence("#{...}"); case "ObjectProperty": return "(object property)"; + case "TupleLiteral": + return fence("#[...]"); default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index a209a9ff59..d9c8b85dee 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -232,6 +232,14 @@ describe("compiler: parser", () => { ]); }); + describe("tuple literals", () => { + parseEach([ + `alias A = #["abc"];`, + `alias A = #["abc", 123];`, + `alias A = #["abc", 123, #{nested: true}];`, + ]); + }); + describe("valueof expressions", () => { parseEach([ "alias A = valueof string;", diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 9df9ab482f..1e624011cb 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -66,9 +66,11 @@ export function getTypeSignature(type: Type | ValueType): string { case "Projection": return "(projection)"; case "Object": - return "(object)"; + return "#{...}"; case "ObjectProperty": return "(object property)"; + case "TupleLiteral": + return "#[...]"; default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); From 9a6d548a63bb1de9d9a7206395501b1f8f905a52 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 14 Mar 2024 15:11:51 -0700 Subject: [PATCH 003/184] fix formatter --- packages/compiler/src/formatter/print/printer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 117a0ee3d7..af051ab4ec 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -997,7 +997,7 @@ export function printObjectLiteral( if (nodeHasComments) { body.push(printDanglingComments(path, options, { sameIndent: true })); } - return group(["{", ifBreak("", " "), indent(body), lineDoc, ifBreak("", " "), "}"]); + return group(["#{", ifBreak("", " "), indent(body), lineDoc, ifBreak("", " "), "}"]); } export function printObjectLiteralProperty( @@ -1024,7 +1024,7 @@ export function printTupleLiteral( print: PrettierChildPrint ) { return group([ - "[", + "#[", indent( join( ", ", From 97effa8b2e04139fac983188c834763b5e54d2ad Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 14 Mar 2024 15:58:17 -0700 Subject: [PATCH 004/184] Simplify --- packages/compiler/src/core/checker.ts | 63 +++++++++------------------ packages/compiler/src/core/types.ts | 12 +---- 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3b5b0fbd78..8e2cea70bb 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -95,8 +95,6 @@ import { NumericLiteralNode, ObjectLiteral, ObjectLiteralNode, - ObjectLiteralProperty, - ObjectLiteralPropertyNode, Operation, OperationStatementNode, Projection, @@ -2966,30 +2964,31 @@ export function createChecker(program: Program): Checker { } function checkObjectLiteral(node: ObjectLiteralNode, mapper: TypeMapper | undefined) { - const type: ObjectLiteral = createType({ - kind: "Object", + return createAndFinishType({ + kind: "ObjectLiteral", node: node, properties: checkObjectLiteralProperties(node, mapper), - indexer: undefined, - decorators: [], - derivedModels: [], }); - return finishType(type); } - function checkObjectLiteralProperties(node: ObjectLiteralNode, mapper: TypeMapper | undefined) { - const properties = createRekeyableMap(); + function checkObjectLiteralProperties( + node: ObjectLiteralNode, + mapper: TypeMapper | undefined + ): Map { + const properties = new Map(); for (const prop of node.properties!) { if ("id" in prop) { - const newProp = checkObjectLiteralProperty(prop, mapper); - if (newProp) { - properties.set(newProp.name, newProp); + const type = getTypeForNode(prop.value, mapper); + if (checkIsLiteralType(type, prop.value)) { + properties.set(prop.id.sv, type); } } else { - const newProperties = checkObjectSpreadProperty(prop.target, mapper); - for (const newProp of newProperties) { - properties.set(newProp.name, newProp); + const targetType = checkObjectSpreadProperty(prop.target, mapper); + if (targetType) { + for (const [name, value] of targetType.properties) { + properties.set(name, value); + } } } } @@ -3013,40 +3012,21 @@ export function createChecker(program: Program): Checker { return true; } - function checkObjectLiteralProperty( - node: ObjectLiteralPropertyNode, - mapper: TypeMapper | undefined - ): ObjectLiteralProperty | undefined { - const type = getTypeForNode(node.value, mapper); - if (!checkIsLiteralType(type, node.value)) { - return undefined; - } - - return createAndFinishType({ - kind: "ObjectProperty", - node, - name: node.id.sv, - type: type as any, - }); - } - function checkObjectSpreadProperty( targetNode: TypeReferenceNode, mapper: TypeMapper | undefined - ): ObjectLiteralProperty[] { + ): ObjectLiteral | undefined { const targetType = getTypeForNode(targetNode, mapper); if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) { - return []; + return undefined; } - // TODO: instanceof is because of conflict of the Object type from projection. - if (targetType.kind !== "Object" || !("values" in targetType.properties)) { + if (targetType.kind !== "ObjectLiteral") { reportCheckerDiagnostic(createDiagnostic({ code: "spread-object", target: targetNode })); - return []; + return undefined; } - // TODO: do we want to clone or use the exact same reference here? - return [...(targetType.properties as any).values()].map((prop) => cloneType(prop)); + return targetType; } function checkTupleLiteral(node: TupleLiteralNode, mapper: TypeMapper | undefined): TupleLiteral { @@ -5309,8 +5289,7 @@ export function createChecker(program: Program): Checker { switch (base.kind) { case "Object": - // TODO: resolve conflict here - return (base.properties as any)[member] || errorType; + return base.properties[member] || errorType; default: const typeOps = projectionMembers[base.kind]; if (!typeOps) { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 5522b5b67e..fc52a46d0f 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -110,7 +110,6 @@ export type Type = | FunctionParameter | ObjectType | ObjectLiteral - | ObjectLiteralProperty | TupleLiteral | Projection; @@ -293,15 +292,8 @@ export interface ModelProperty extends BaseType, DecoratedType { } export interface ObjectLiteral extends BaseType { - kind: "Object"; - properties: RekeyableMap; -} - -export interface ObjectLiteralProperty extends BaseType { - kind: "ObjectProperty"; - name: string; - node: ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode; - type: LiteralType | ObjectLiteral | TupleLiteral; + kind: "ObjectLiteral"; + properties: Map; } export interface TupleLiteral extends BaseType { From 6a263cf3f04643f20ee2230ff3f4e3282b209d18 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 14 Mar 2024 16:00:21 -0700 Subject: [PATCH 005/184] Fixes --- packages/compiler/src/core/parser.ts | 2 +- packages/compiler/src/core/semantic-walker.ts | 4 ++-- packages/compiler/src/server/type-signature.ts | 4 ++-- packages/tspd/src/ref-doc/utils/type-signature.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 646e03419c..87fd32bfe8 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1009,7 +1009,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }); parseExpected(Token.Colon); - const value = parseExpression() as any; // TODO? only parse object expressions or let checker verify that? + const value = parseExpression(); return { kind: SyntaxKind.ObjectLiteralProperty, diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index c65417b702..6a0e4f2f16 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -95,7 +95,7 @@ export function scopeNavigationToNamespace( return ListenerFlow.NoRecursion; } } - return (callback as any)(x as any); + return callback(x as any); }; } return wrappedListeners as any; @@ -384,6 +384,7 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { case "Decorator": return navigateDecoratorDeclaration(type, context); case "Object": + case "ObjectLiteral": case "TupleLiteral": case "Projection": case "Function": @@ -393,7 +394,6 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { case "Intrinsic": case "Number": case "String": - case "ObjectProperty": return; default: // Dummy const to ensure we handle all types. diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index 6351e10969..5516407034 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -76,9 +76,9 @@ function getTypeSignature(type: Type | ValueType): string { case "Projection": return "(projection)"; case "Object": + return "(object)"; + case "ObjectLiteral": return fence("#{...}"); - case "ObjectProperty": - return "(object property)"; case "TupleLiteral": return fence("#[...]"); default: diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 1e624011cb..0c93044f04 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -66,9 +66,9 @@ export function getTypeSignature(type: Type | ValueType): string { case "Projection": return "(projection)"; case "Object": + return "(object)"; + case "ObjectLiteral": return "#{...}"; - case "ObjectProperty": - return "(object property)"; case "TupleLiteral": return "#[...]"; default: From 320797c6fc239a46b0cf0fda9cbda6b831f45678 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 08:51:00 -0700 Subject: [PATCH 006/184] Add tests for basic checking --- packages/compiler/src/core/checker.ts | 2 +- packages/compiler/src/core/messages.ts | 2 +- .../test/checker/object-literals.test.ts | 209 ++++++++++++++++++ 3 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/checker/object-literals.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 8e2cea70bb..1528d4c48d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3000,7 +3000,7 @@ export function createChecker(program: Program): Checker { diagnosticTarget: DiagnosticTarget ): type is LiteralType | ObjectLiteral | TupleLiteral { if ( - type.kind !== "Object" && + type.kind !== "ObjectLiteral" && type.kind !== "TupleLiteral" && type.kind !== "String" && type.kind !== "Number" && diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 896da90dd7..f9ccb20b0d 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -407,7 +407,7 @@ const diagnostics = { "not-literal": { severity: "error", messages: { - default: "Type must be a literal type.", + default: "Type must be a literal type.", // TODO: better message? Goal here is to say you needed to use another literal type inside a literal object or tuple. }, }, unassignable: { diff --git a/packages/compiler/test/checker/object-literals.test.ts b/packages/compiler/test/checker/object-literals.test.ts new file mode 100644 index 0000000000..0ef0647461 --- /dev/null +++ b/packages/compiler/test/checker/object-literals.test.ts @@ -0,0 +1,209 @@ +import { ok, strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { Diagnostic, Type } from "../../src/index.js"; +import { + createTestHost, + expectDiagnosticEmpty, + expectDiagnostics, +} from "../../src/testing/index.js"; + +async function compileAndDiagnoseValueType( + code: string, + other?: string +): Promise<[Type | undefined, readonly Diagnostic[]]> { + const host = await createTestHost(); + let called: Type | undefined; + host.addJsFile("dec.js", { + $collect: (context: DecoratorContext, target: Type, value: Type) => { + called = value; + }, + }); + host.addTypeSpecFile( + "main.tsp", + ` + import "./dec.js"; + + @collect(${code}) + model Test {} + + ${other ?? ""} + ` + ); + const diagnostics = await host.diagnose("main.tsp"); + return [called, diagnostics]; +} + +async function compileValueType(code: string, other?: string): Promise { + const [called, diagnostics] = await compileAndDiagnoseValueType(code, other); + expectDiagnosticEmpty(diagnostics); + ok(called, "Decorator was not called"); + + return called; +} + +async function diagnoseValueType(code: string, other?: string): Promise { + const [_, diagnostics] = await compileAndDiagnoseValueType(code, other); + return diagnostics; +} + +describe("object literals", () => { + it("no properties", async () => { + const object = await compileValueType(`#{}`); + strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.properties.size, 0); + }); + + it("single property", async () => { + const object = await compileValueType(`#{name: "John"}`); + strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.properties.size, 1); + const nameProp = object.properties.get("name"); + strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp.value, "John"); + }); + + it("multiple property", async () => { + const object = await compileValueType(`#{name: "John", age: 21}`); + strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.properties.size, 2); + + const nameProp = object.properties.get("name"); + strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.properties.get("age"); + strictEqual(ageProp?.kind, "Number"); + strictEqual(ageProp.value, 21); + }); + + describe("spreading", () => { + it("add the properties", async () => { + const object = await compileValueType( + `#{...Common, age: 21}`, + `alias Common = #{ name: "John" };` + ); + strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.properties.size, 2); + + const nameProp = object.properties.get("name"); + strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.properties.get("age"); + strictEqual(ageProp?.kind, "Number"); + strictEqual(ageProp.value, 21); + }); + + it("override properties defined before if there is a name conflict", async () => { + const object = await compileValueType( + `#{name: "John", age: 21, ...Common, }`, + `alias Common = #{ name: "Common" };` + ); + strictEqual(object.kind, "ObjectLiteral"); + + const nameProp = object.properties.get("name"); + strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp.value, "Common"); + }); + + it("override properties spread before", async () => { + const object = await compileValueType( + `#{...Common, name: "John", age: 21 }`, + `alias Common = #{ name: "John" };` + ); + strictEqual(object.kind, "ObjectLiteral"); + + const nameProp = object.properties.get("name"); + strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp.value, "John"); + }); + + it("emit diagnostic is spreading something else than an object literal", async () => { + const diagnostics = await diagnoseValueType( + `#{...Common, age: 21}`, + `alias Common = { name: "John" };` + ); + expectDiagnostics(diagnostics, { + code: "spread-object", + message: "Cannot spread properties of non-object type.", + }); + }); + }); + + describe("valid property types", () => { + it.each([ + ["String", `"John"`], + ["Number", "21"], + ["Boolean", "true"], + ["ObjectLiteral", `#{nested: "foo"}`], + ["TupleLiteral", `#["foo"]`], + ])("%s", async (kind, type) => { + const object = await compileValueType(`#{prop: ${type}}`); + strictEqual(object.kind, "ObjectLiteral"); + const nameProp = object.properties.get("prop"); + strictEqual(nameProp?.kind, kind); + }); + }); + + it("emit diagnostic if referencing a non literal type", async () => { + const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); + expectDiagnostics(diagnostics, { + code: "not-literal", + message: "Type must be a literal type.", + }); + }); +}); + +describe.only("tuple literals", () => { + it("no values", async () => { + const object = await compileValueType(`#[]`); + strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.values.length, 0); + }); + + it("single value", async () => { + const object = await compileValueType(`#["John"]`); + strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.values.length, 1); + const first = object.values[0]; + strictEqual(first.kind, "String"); + strictEqual(first.value, "John"); + }); + + it("multiple property", async () => { + const object = await compileValueType(`#["John", 21]`); + strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.values.length, 2); + + const nameProp = object.values[0]; + strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.values[1]; + strictEqual(ageProp?.kind, "Number"); + strictEqual(ageProp.value, 21); + }); + + describe("valid property types", () => { + it.each([ + ["String", `"John"`], + ["Number", "21"], + ["Boolean", "true"], + ["ObjectLiteral", `#{nested: "foo"}`], + ["TupleLiteral", `#["foo"]`], + ])("%s", async (kind, type) => { + const object = await compileValueType(`#[${type}]`); + strictEqual(object.kind, "TupleLiteral"); + const nameProp = object.values[0]; + strictEqual(nameProp?.kind, kind); + }); + }); + + it("emit diagnostic if referencing a non literal type", async () => { + const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); + expectDiagnostics(diagnostics, { + code: "not-literal", + message: "Type must be a literal type.", + }); + }); +}); From 69484555dea9eb0d4895e4346b7b8736cca6da6f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 09:33:25 -0700 Subject: [PATCH 007/184] Tm language and spec --- grammars/typespec.json | 82 +++++++++++++ packages/compiler/src/server/classify.ts | 1 + packages/compiler/src/server/tmlanguage.ts | 45 ++++++- .../test/checker/object-literals.test.ts | 2 +- .../compiler/test/server/colorization.test.ts | 115 ++++++++++++++++++ packages/spec/src/spec.emu.html | 22 ++++ 6 files changed, 265 insertions(+), 2 deletions(-) diff --git a/grammars/typespec.json b/grammars/typespec.json index a7472e4cbf..9c022a7d70 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -275,6 +275,12 @@ { "include": "#type-arguments" }, + { + "include": "#object-literal" + }, + { + "include": "#tuple-literal" + }, { "include": "#tuple-expression" }, @@ -649,6 +655,59 @@ "name": "constant.numeric.tsp", "match": "(?:\\b(? { }); }); -describe.only("tuple literals", () => { +describe("tuple literals", () => { it("no values", async () => { const object = await compileValueType(`#[]`); strictEqual(object.kind, "TupleLiteral"); diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 5a240b8a62..131a201d02 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -81,6 +81,8 @@ const Token = { closeBrace: createToken("}", "punctuation.curlybrace.close.tsp"), openParen: createToken("(", "punctuation.parenthesis.open.tsp"), closeParen: createToken(")", "punctuation.parenthesis.close.tsp"), + openHashBrace: createToken("#{", "punctuation.hashcurlybrace.open.tsp"), + openHashBracket: createToken("#[", "punctuation.hashsquarebracket.open.tsp"), semicolon: createToken(";", "punctuation.terminator.statement.tsp"), typeParameters: { @@ -939,6 +941,109 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); + describe("object literals", () => { + it("empty", async () => { + const tokens = await tokenizeWithAlias("#{}"); + deepStrictEqual(tokens, [Token.punctuation.openHashBrace, Token.punctuation.closeBrace]); + }); + + it("single prop", async () => { + const tokens = await tokenizeWithAlias(`#{name: "John"}`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBrace, + Token.identifiers.variable("name"), + Token.operators.typeAnnotation, + Token.literals.stringQuoted("John"), + Token.punctuation.closeBrace, + ]); + }); + + it("multiple prop", async () => { + const tokens = await tokenizeWithAlias(`#{name: "John", age: 21}`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBrace, + Token.identifiers.variable("name"), + Token.operators.typeAnnotation, + Token.literals.stringQuoted("John"), + Token.punctuation.comma, + Token.identifiers.variable("age"), + Token.operators.typeAnnotation, + Token.literals.numeric("21"), + Token.punctuation.closeBrace, + ]); + }); + + it("spreading prop", async () => { + const tokens = await tokenizeWithAlias(`#{name: "John", ...Common}`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBrace, + Token.identifiers.variable("name"), + Token.operators.typeAnnotation, + Token.literals.stringQuoted("John"), + Token.punctuation.comma, + Token.operators.spread, + Token.identifiers.type("Common"), + Token.punctuation.closeBrace, + ]); + }); + + it("nested prop", async () => { + const tokens = await tokenizeWithAlias(`#{prop: #{age: 21}}`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBrace, + Token.identifiers.variable("prop"), + Token.operators.typeAnnotation, + Token.punctuation.openHashBrace, + Token.identifiers.variable("age"), + Token.operators.typeAnnotation, + Token.literals.numeric("21"), + Token.punctuation.closeBrace, + Token.punctuation.closeBrace, + ]); + }); + }); + + describe("tuple literals", () => { + it("empty", async () => { + const tokens = await tokenizeWithAlias("#[]"); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBracket, + Token.punctuation.closeBracket, + ]); + }); + + it("single value", async () => { + const tokens = await tokenizeWithAlias(`#["John"]`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBracket, + Token.literals.stringQuoted("John"), + Token.punctuation.closeBracket, + ]); + }); + + it("multiple values", async () => { + const tokens = await tokenizeWithAlias(`#["John", 21]`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBracket, + Token.literals.stringQuoted("John"), + Token.punctuation.comma, + Token.literals.numeric("21"), + Token.punctuation.closeBracket, + ]); + }); + + it("nested tuple", async () => { + const tokens = await tokenizeWithAlias(`#[#[21]]`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBracket, + Token.punctuation.openHashBracket, + Token.literals.numeric("21"), + Token.punctuation.closeBracket, + Token.punctuation.closeBracket, + ]); + }); + }); + describe("decorator declarations", () => { it("extern decorator", async () => { const tokens = await tokenize("extern dec tag(target: Namespace);"); @@ -1242,6 +1347,16 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); }); + + async function tokenizeWithAlias(text: string) { + const common = [Token.keywords.alias, Token.identifiers.type("T"), Token.operators.assignment]; + const tokens = await tokenize(`alias T = ${text}`); + for (let i = 0; i < common.length; i++) { + deepStrictEqual(tokens[i], common[i]); + } + + return tokens.slice(common.length); + } } const punctuationMap = getPunctuationMap(); diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 46840457fc..4b6a60cf17 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -478,6 +478,8 @@

Syntactic Grammar

Literal ReferenceExpression ParenthesizedExpression + ObjectLiteral + TupleLiteral ModelExpression TupleExpression @@ -506,6 +508,26 @@

Syntactic Grammar

ParenthesizedExpression : `(` Expression `)` +ObjectLiteral : + `#{` ObjectLiteralBody? `}` + +ObjectLiteralBody : + ModelPropertyList `,`? + +ObjectLiteralPropertyList : + ObjectLiteralProperty + ObjectLiteralPropertyList `,` ObjectLiteralProperty + +ObjectLiteralProperty : + ObjectLiteralSpreadProperty + Identifier `:` Expression + +ObjectLiteralSpreadProperty : + `...` ReferenceExpression + +TupleLiteral : + `#[` ExpressionList? `]` + ModelExpression : `{` ModelBody? `}` From 3560171a8abf5d7a38ae6182ba841a235338d496 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 10:28:39 -0700 Subject: [PATCH 008/184] Add valueof assignment --- packages/compiler/src/core/checker.ts | 133 ++++++++++++++++-- .../src/core/helpers/type-name-utils.ts | 4 + .../compiler/test/checker/relation.test.ts | 109 ++++++++++++++ 3 files changed, 237 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 1528d4c48d..4b8cdeca46 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -39,6 +39,7 @@ import { import { AliasStatementNode, ArrayExpressionNode, + ArrayModelType, AugmentDecoratorStatementNode, BooleanLiteral, BooleanLiteralNode, @@ -3680,7 +3681,7 @@ export function createChecker(program: Program): Checker { if (type.kind === "UnionVariant") { return isValueType(type.type); } - const valueTypes = new Set(["String", "Number", "Boolean", "EnumMember", "Tuple"]); + const valueTypes = new Set(["String", "Number", "Boolean", "ObjectLiteral", "TupleLiteral"]); return valueTypes.has(type.kind); } @@ -5659,7 +5660,23 @@ export function createChecker(program: Program): Checker { ); } else if (target.kind === "Model" && source.kind === "Model") { return isModelRelatedTo(source, target, diagnosticTarget, relationCache); - } else if (target.kind === "Model" && target.indexer && source.kind === "Tuple") { + } else if ( + target.kind === "Model" && + !isArrayModelType(program, target) && + source.kind === "ObjectLiteral" + ) { + return isObjectLiteralOfModelType(source, target, diagnosticTarget, relationCache); + } else if ( + target.kind === "Model" && + isArrayModelType(program, target) && + source.kind === "TupleLiteral" + ) { + return isTupleLiteralOfArrayType(source, target, diagnosticTarget, relationCache); + } else if ( + target.kind === "Model" && + isArrayModelType(program, target) && + (source.kind === "Tuple" || source.kind === "TupleLiteral") + ) { for (const item of source.values) { const [related, diagnostics] = isTypeAssignableToInternal( item, @@ -5672,7 +5689,10 @@ export function createChecker(program: Program): Checker { } } return [Related.true, []]; - } else if (target.kind === "Tuple" && source.kind === "Tuple") { + } else if ( + target.kind === "Tuple" && + (source.kind === "Tuple" || source.kind === "TupleLiteral") + ) { return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); } else if (target.kind === "Union") { return isAssignableToUnion(source, target, diagnosticTarget, relationCache); @@ -5843,6 +5863,79 @@ export function createChecker(program: Program): Checker { return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; } + function isObjectLiteralOfModelType( + source: ObjectLiteral, + target: Model, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + ): [Related, readonly Diagnostic[]] { + relationCache.set([source, target], Related.maybe); + const diagnostics: Diagnostic[] = []; + const remainingProperties = new Map(source.properties); + for (const prop of walkPropertiesInherited(target)) { + const sourceProperty = source.properties.get(prop.name); + if (sourceProperty === undefined) { + if (!prop.optional) { + diagnostics.push( + createDiagnostic({ + code: "missing-property", + format: { + propertyName: prop.name, + sourceType: getTypeName(source), + targetType: getTypeName(target), + }, + target: source, + }) + ); + } + } else { + remainingProperties.delete(prop.name); + const [related, propDiagnostics] = isTypeAssignableToInternal( + sourceProperty, + prop.type, + diagnosticTarget, + relationCache + ); + if (!related) { + diagnostics.push(...propDiagnostics); + } + } + } + + if (target.indexer) { + const [_, indexerDiagnostics] = arePropertiesAssignableToIndexer( + remainingProperties, + target.indexer.value, + diagnosticTarget, + relationCache + ); + diagnostics.push(...indexerDiagnostics); + } + return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; + } + + function isTupleLiteralOfArrayType( + source: TupleLiteral, + target: ArrayModelType, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + ): [Related, readonly Diagnostic[]] { + relationCache.set([source, target], Related.maybe); + for (const value of source.values) { + const [related, diagnostics] = isTypeAssignableToInternal( + value, + target.indexer.value, + diagnosticTarget, + relationCache + ); + if (!related) { + return [Related.false, diagnostics]; + } + } + + return [Related.true, []]; + } + function getProperty(model: Model, name: string): ModelProperty | undefined { return ( model.properties.get(name) ?? @@ -5850,6 +5943,27 @@ export function createChecker(program: Program): Checker { ); } + function arePropertiesAssignableToIndexer( + properties: Map, + indexerConstaint: Type, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + ): [Related, readonly Diagnostic[]] { + for (const prop of properties.values()) { + const [related, diagnostics] = isTypeAssignableToInternal( + prop, + indexerConstaint, + diagnosticTarget, + relationCache + ); + if (!related) { + return [Related.false, diagnostics]; + } + } + + return [Related.true, []]; + } + function isIndexerValid( source: Model, target: Model & { indexer: ModelIndexer }, @@ -5858,7 +5972,7 @@ export function createChecker(program: Program): Checker { ): [Related, readonly Diagnostic[]] { // Model expressions should be able to be assigned. if (source.name === "" && target.indexer.key.name !== "integer") { - return isIndexConstraintValid(target.indexer.value, source, diagnosticTarget, relationCache); + return isIndexConstraintValid(source, target.indexer.value, diagnosticTarget, relationCache); } else { if (source.indexer === undefined || source.indexer.key !== target.indexer.key) { return [ @@ -5889,16 +6003,17 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Diagnostic target unless something better can be inferred. */ function isIndexConstraintValid( - constraintType: Type, type: Model, + constraintType: Type, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> ): [Related, readonly Diagnostic[]] { for (const prop of type.properties.values()) { - const [related, diagnostics] = isTypeAssignableTo( + const [related, diagnostics] = isTypeAssignableToInternal( prop.type, constraintType, - diagnosticTarget + diagnosticTarget, + relationCache ); if (!related) { return [Related.false, diagnostics]; @@ -5907,8 +6022,8 @@ export function createChecker(program: Program): Checker { if (type.baseModel) { const [related, diagnostics] = isIndexConstraintValid( - constraintType, type.baseModel, + constraintType, diagnosticTarget, relationCache ); @@ -5920,7 +6035,7 @@ export function createChecker(program: Program): Checker { } function isTupleAssignableToTuple( - source: Tuple, + source: Tuple | TupleLiteral, target: Tuple, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 8bbdd1e8df..16ea55d72f 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -56,6 +56,10 @@ export function getTypeName(type: Type | ValueType, options?: TypeNameOptions): return type.name; case "Value": return `valueof ${getTypeName(type.target, options)}`; + case "ObjectLiteral": + return `#{${[...type.properties.entries()].map(([name, value]) => `${name}: ${getTypeName(value, options)}`).join(", ")}}`; + case "TupleLiteral": + return `#[${type.values.map((x) => getTypeName(x, options)).join(", ")}]`; } return "(unnamed type)"; diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index cdf62cf1f9..f1ff872ab8 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1195,6 +1195,115 @@ describe("compiler: checker: type relations", () => { }); }); + describe("valueof model", () => { + it("can assign object literal", async () => { + await expectTypeAssignable({ + source: `#{name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }); + }); + + it("can assign object literal with optional properties", async () => { + await expectTypeAssignable({ + source: `#{name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { name: string, age?: int32 }`, + }); + }); + + it("cannot assign a model ", async () => { + await expectTypeNotAssignable( + { + source: `{name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }, + { + code: "unassignable", + message: "Type '(anonymous model)' is not assignable to type 'valueof Info'", + } + ); + }); + + it("cannot assign a tuple literal", async () => { + await expectTypeNotAssignable( + { + source: `#["foo"]`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }, + { + code: "unassignable", + message: `Type '#["foo"]' is not assignable to type 'Info'`, + } + ); + }); + + it("cannot assign string scalar", async () => { + await expectTypeNotAssignable( + { source: `string`, target: "valueof Info", commonCode: `model Info { name: string }` }, + { + code: "unassignable", + message: "Type 'string' is not assignable to type 'Info'", + } + ); + }); + }); + + describe("valueof array", () => { + it("can assign tuple literal", async () => { + await expectTypeAssignable({ + source: `#["foo"]`, + target: "valueof string[]", + }); + }); + + it("can assign tuple literal of object literal", async () => { + await expectTypeAssignable({ + source: `#[#{name: "a"}, #{name: "b"}]`, + target: "valueof Info[]", + commonCode: `model Info { name: string }`, + }); + }); + + it("cannot assign a tuple", async () => { + await expectTypeNotAssignable( + { + source: `["foo"]`, + target: "valueof string[]", + }, + { + code: "unassignable", + message: `Type '["foo"]' is not assignable to type 'valueof string[]'`, + } + ); + }); + + it("cannot assign an object literal", async () => { + await expectTypeNotAssignable( + { + source: `#{name: "foo"}`, + target: "valueof string[]", + }, + { + code: "unassignable", + message: `Type '#{name: "foo"}' is not assignable to type 'string[]'`, + } + ); + }); + + it("cannot assign string scalar", async () => { + await expectTypeNotAssignable( + { source: `string`, target: "valueof string[]" }, + { + code: "unassignable", + message: "Type 'string' is not assignable to type 'string[]'", + } + ); + }); + }); + it("can use valueof in template parameter constraints", async () => { const diagnostics = await runner.diagnose(` model Foo { From 898beb6f492ebbfdc168b27f2443043c5f1ffdd1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 10:56:18 -0700 Subject: [PATCH 009/184] Marshall types --- packages/compiler/src/core/checker.ts | 39 +++------------- packages/compiler/src/core/js-marshaller.ts | 45 +++++++++++++++++++ packages/compiler/src/core/types.ts | 4 +- .../compiler/test/checker/decorators.test.ts | 34 +++++++++++++- 4 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 packages/compiler/src/core/js-marshaller.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 4b8cdeca46..c6c80ff0ff 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -12,13 +12,9 @@ import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-iden import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; -import { - TypeNameOptions, - getNamespaceFullName, - getTypeName, - stringTemplateToString, -} from "./helpers/index.js"; +import { TypeNameOptions, getNamespaceFullName, getTypeName } from "./helpers/index.js"; import { isStringTemplateSerializable } from "./helpers/string-template-utils.js"; +import { marshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { exprIsBareIdentifier, @@ -74,7 +70,6 @@ import { JsSourceFileNode, LiteralNode, LiteralType, - MarshalledValue, MemberContainerNode, MemberContainerType, MemberExpressionNode, @@ -3866,11 +3861,7 @@ export function createChecker(program: Program): Checker { function resolveDecoratorArgJsValue(value: Type, valueOf: boolean) { if (valueOf) { - if (value.kind === "Boolean" || value.kind === "String" || value.kind === "Number") { - return literalTypeToValue(value); - } else if (value.kind === "StringTemplate") { - return stringTemplateToString(value)[0]; - } + return marshallTypeForJS(value); } return value; } @@ -5381,7 +5372,7 @@ export function createChecker(program: Program): Checker { if (!ref) throw new ProjectionError("Can't find decorator."); compilerAssert(ref.flags & SymbolFlags.Decorator, "should only resolve decorator symbols"); return createFunctionType((...args: Type[]): Type => { - ref.value!({ program }, ...marshalArgumentsForJS(args)); + ref.value!({ program }, ...args.map(marshallTypeForJS)); return voidType; }); } @@ -5408,7 +5399,7 @@ export function createChecker(program: Program): Checker { } else if (ref.flags & SymbolFlags.Function) { // TODO: store this in a symbol link probably? const t: FunctionType = createFunctionType((...args: Type[]): Type => { - const retval = ref.value!(program, ...marshalArgumentsForJS(args)); + const retval = ref.value!(program, ...args.map(marshallTypeForJS)); return marshalProjectionReturn(retval, { functionName: node.sv }); }); return t; @@ -6611,26 +6602,6 @@ function createDecoratorContext(program: Program, decApp: DecoratorApplication): }; } -/** - * Convert TypeSpec argument to JS argument. - */ -function marshalArgumentsForJS(args: T[]): MarshalledValue[] { - return args.map((arg) => { - if (arg.kind === "Boolean" || arg.kind === "String" || arg.kind === "Number") { - return literalTypeToValue(arg); - } else if (arg.kind === "StringTemplate") { - return stringTemplateToString(arg)[0]; - } - return arg as any; - }); -} - -function literalTypeToValue( - type: T -): MarshalledValue { - return type.value as any; -} - function isTemplatedNode(node: Node): node is TemplateableNode { return "templateParameters" in node && node.templateParameters.length > 0; } diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts new file mode 100644 index 0000000000..b0e379351c --- /dev/null +++ b/packages/compiler/src/core/js-marshaller.ts @@ -0,0 +1,45 @@ +import { stringTemplateToString } from "./index.js"; +import type { + BooleanLiteral, + MarshalledValue, + NumericLiteral, + ObjectLiteral, + StringLiteral, + TupleLiteral, + Type, +} from "./types.js"; + +export function marshallTypeForJS(type: T): MarshalledValue { + switch (type.kind) { + case "Boolean": + case "String": + case "Number": + return literalTypeToValue(type) as any; + case "StringTemplate": + return stringTemplateToString(type)[0] as any; + case "ObjectLiteral": + return objectLiteralToValue(type) as any; + case "TupleLiteral": + return tupleLiteralToValue(type) as any; + // In other case we keep the original tye + default: + return type as any; + } +} + +function literalTypeToValue( + type: T +): MarshalledValue { + return type.value as any; +} + +function objectLiteralToValue(type: ObjectLiteral) { + const result: Record = {}; + for (const [key, value] of type.properties) { + result[key] = marshallTypeForJS(value); + } + return result; +} +function tupleLiteralToValue(type: TupleLiteral) { + return type.values.map(marshallTypeForJS); +} diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index fc52a46d0f..d1fddeebb8 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -11,6 +11,8 @@ export type MarshalledValue = Type extends StringLiteral ? string : Type extends NumericLiteral ? number : Type extends BooleanLiteral ? boolean + : Type extends ObjectLiteral ? Record + : Type extends TupleLiteral ? unknown[] : Type /** @@ -24,7 +26,7 @@ export interface DecoratorArgument { /** * Marshalled value for use in Javascript. */ - jsValue: Type | string | number | boolean; + jsValue: Type | Record | unknown[] | string | number | boolean; node?: Node; } diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 06964b1333..6cae995993 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,4 +1,4 @@ -import { ok, strictEqual } from "assert"; +import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { setTypeSpecNamespace } from "../../src/core/index.js"; import { @@ -339,6 +339,38 @@ describe("compiler: checker: decorators", () => { strictEqual(arg, true); }); }); + + describe("passing an object literal", () => { + it("valueof model cast the value to a JS object", async () => { + const arg = await testCallDecorator("valueof {name: string}", `#{name: "foo"}`); + deepStrictEqual(arg, { name: "foo" }); + }); + + it("valueof model cast the value recursively to a JS object", async () => { + const arg = await testCallDecorator( + "valueof {name: unknown}", + `#{name: #{other: "foo"}}` + ); + deepStrictEqual(arg, { name: { other: "foo" } }); + }); + + it("`: {...}` keeps the ObjectLiteral type", async () => { + const arg = await testCallDecorator("{name: string}", `#{name: "foo"}`); + strictEqual(arg.kind, "ObjectLiteral"); + }); + }); + + describe("passing an tuple literal", () => { + it("valueof model cast the value to a JS array", async () => { + const arg = await testCallDecorator("valueof string[]", `#["foo"]`); + deepStrictEqual(arg, ["foo"]); + }); + + it("valueof model cast the value recursively to a JS object", async () => { + const arg = await testCallDecorator("valueof unknown[]", `#[#["foo"]]`); + deepStrictEqual(arg, [["foo"]]); + }); + }); }); }); From 91b4d45f4898e9a5752505b1787b98025452caca Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 10:57:33 -0700 Subject: [PATCH 010/184] project --- packages/compiler/src/core/projector.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index 4716174afe..21a455d6eb 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -545,7 +545,10 @@ export function createProjector( for (const dec of decs) { const args: DecoratorArgument[] = []; for (const arg of dec.args) { - const jsValue = typeof arg.jsValue === "object" ? projectType(arg.jsValue) : arg.jsValue; + const jsValue = + typeof arg.jsValue === "object" && "kind" in arg.jsValue + ? projectType(arg.jsValue as any) + : arg.jsValue; args.push({ ...arg, value: projectType(arg.value), jsValue }); } From 8376eeef4ab76e44877ab86ab89ec3de4519b49b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 11:14:37 -0700 Subject: [PATCH 011/184] Handle defaults --- packages/compiler/test/checker/model.test.ts | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 5cdaefcd88..6ddbf14dec 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -124,6 +124,28 @@ describe("compiler: models", () => { deepStrictEqual({ ...foo.default }, expectedValue); }); } + + it(`foo?: string[] = #["abc"]`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: string[] = #["abc"] } + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + deepStrictEqual(foo.default?.kind, "TupleLiteral"); + }); + + it(`foo?: {name: string} = #{name: "abc"}`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: {name: string} = #{name: "abc"} } + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + deepStrictEqual(foo.default?.kind, "ObjectLiteral"); + }); }); describe("doesn't allow a default of different type than the property type", () => { @@ -131,7 +153,7 @@ describe("compiler: models", () => { ["string", "123", "Type '123' is not assignable to type 'string'"], ["int32", `"foo"`, `Type '"foo"' is not assignable to type 'int32'`], ["boolean", `"foo"`, `Type '"foo"' is not assignable to type 'boolean'`], - ["string[]", `["foo", 123]`, `Type '123' is not assignable to type 'string'`], + ["string[]", `#["foo", 123]`, `Type '123' is not assignable to type 'string'`], [`"foo" | "bar"`, `"foo1"`, `Type '"foo1"' is not assignable to type '"foo" | "bar"'`], ]; From e23685fd58aa2dc592099c1f7ff255f116c3dbce Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 11:34:48 -0700 Subject: [PATCH 012/184] Fix --- packages/http/lib/auth.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http/lib/auth.tsp b/packages/http/lib/auth.tsp index ec4a39ba4f..b142ffaee4 100644 --- a/packages/http/lib/auth.tsp +++ b/packages/http/lib/auth.tsp @@ -111,7 +111,7 @@ model ApiKeyAuth { * @template Scopes The list of OAuth2 scopes, which are common for every flow from `Flows`. This list is combined with the scopes defined in specific OAuth2 flows. */ @doc("") -model OAuth2Auth { +model OAuth2Auth { @doc("OAuth2 authentication") type: AuthType.oauth2; From 80518d2bfd0e7078880891d8fae746979645ebb1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 11:41:18 -0700 Subject: [PATCH 013/184] Create feature-object-literals-2024-2-15-18-36-3.md --- ...feature-object-literals-2024-2-15-18-36-3.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .chronus/changes/feature-object-literals-2024-2-15-18-36-3.md diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md new file mode 100644 index 0000000000..4d06c1a9f6 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md @@ -0,0 +1,17 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +New Language Feature: Object and Tuple Literals. + +```tsp +@dummy(#{ + name: "John", + age: 48, + address: #{ city: "London" } + aliases: #["Bob", "Frank"] +}) +``` From 29b49f85806fdefe927c1bc9f9eecb8154e41481 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 11:41:49 -0700 Subject: [PATCH 014/184] Create feature-object-literals-32024-2-15-18-36-3.md --- .../changes/feature-object-literals-32024-2-15-18-36-3.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/feature-object-literals-32024-2-15-18-36-3.md diff --git a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md new file mode 100644 index 0000000000..b2909027a0 --- /dev/null +++ b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/http" +--- + +Update Flow Template to make sure of new tuple literals From a5283b2c4b8940c940529f844681470ecc16c555 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 13:42:00 -0700 Subject: [PATCH 015/184] . --- packages/compiler/src/core/checker.ts | 40 ++++++---- packages/compiler/src/core/messages.ts | 6 ++ .../compiler/test/checker/relation.test.ts | 79 ++++++++++++++++--- 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c6c80ff0ff..5d766d1761 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -666,10 +666,6 @@ export function createChecker(program: Program): Checker { return checkNamespace(node); case SyntaxKind.OperationStatement: return checkOperation(node, mapper); - case SyntaxKind.ObjectLiteral: - return checkObjectLiteral(node, mapper); - case SyntaxKind.TupleLiteral: - return checkTupleLiteral(node, mapper); case SyntaxKind.NumericLiteral: return checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: @@ -704,6 +700,10 @@ export function createChecker(program: Program): Checker { return neverType; case SyntaxKind.UnknownKeyword: return unknownType; + case SyntaxKind.ObjectLiteral: + case SyntaxKind.TupleLiteral: + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return errorType; } // we don't emit an error here as we blindly call this function @@ -797,7 +797,7 @@ export function createChecker(program: Program): Checker { if (node.constraint) { pendingResolutions.start(getNodeSymId(node), ResolutionKind.Constraint); - type.constraint = getTypeOrValueTypeForNode(node.constraint); + type.constraint = getTypeOrValueOfTypeForNode(node.constraint); pendingResolutions.finish(getNodeSymId(node), ResolutionKind.Constraint); } if (node.default) { @@ -888,7 +888,7 @@ export function createChecker(program: Program): Checker { } function checkTemplateArgument(node: TemplateArgumentNode, mapper: TypeMapper | undefined): Type { - return getTypeForNode(node.argument, mapper); + return getTypeOrValueForNode(node.argument, mapper); } function resolveTypeReference( @@ -1428,6 +1428,7 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined ): ValueType { const target = getTypeForNode(node.target, mapper); + return { kind: "Value", target, @@ -1555,7 +1556,7 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "rest-parameter-array", target: node.type }) ); } - const type = node.type ? getTypeOrValueTypeForNode(node.type) : unknownType; + const type = node.type ? getTypeOrValueOfTypeForNode(node.type) : unknownType; const parameterType: FunctionParameter = createType({ kind: "FunctionParameter", @@ -1571,7 +1572,18 @@ export function createChecker(program: Program): Checker { return parameterType; } - function getTypeOrValueTypeForNode(node: Node, mapper?: TypeMapper) { + function getTypeOrValueForNode(node: Node, mapper?: TypeMapper) { + switch (node.kind) { + case SyntaxKind.ObjectLiteral: + return checkObjectLiteral(node, mapper); + case SyntaxKind.TupleLiteral: + return checkTupleLiteral(node, mapper); + default: + return getTypeForNode(node, mapper); + } + } + + function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper) { if (node.kind === SyntaxKind.ValueOfExpression) { return checkValueOfExpression(node, mapper); } @@ -2975,7 +2987,7 @@ export function createChecker(program: Program): Checker { for (const prop of node.properties!) { if ("id" in prop) { - const type = getTypeForNode(prop.value, mapper); + const type = getTypeOrValueForNode(prop.value, mapper); if (checkIsLiteralType(type, prop.value)) { properties.set(prop.id.sv, type); } @@ -3012,7 +3024,7 @@ export function createChecker(program: Program): Checker { targetNode: TypeReferenceNode, mapper: TypeMapper | undefined ): ObjectLiteral | undefined { - const targetType = getTypeForNode(targetNode, mapper); + const targetType = getTypeOrValueForNode(targetNode, mapper); if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) { return undefined; @@ -3028,7 +3040,7 @@ export function createChecker(program: Program): Checker { function checkTupleLiteral(node: TupleLiteralNode, mapper: TypeMapper | undefined): TupleLiteral { const values = node.values .map((itemNode) => { - const type = getTypeForNode(itemNode, mapper); + const type = getTypeOrValueForNode(itemNode, mapper); if (checkIsLiteralType(type, itemNode)) { return type; } else { @@ -3681,7 +3693,7 @@ export function createChecker(program: Program): Checker { } function checkDefault(defaultNode: Node, type: Type): Type { - const defaultType = getTypeForNode(defaultNode, undefined); + const defaultType = getTypeOrValueForNode(defaultNode, undefined); if (isErrorType(type)) { return errorType; } @@ -3941,7 +3953,7 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined ): DecoratorArgument[] { return decorator.arguments.map((argNode): DecoratorArgument => { - const type = getTypeForNode(argNode, mapper); + const type = getTypeOrValueForNode(argNode, mapper); return { value: type, jsValue: type, @@ -4060,7 +4072,7 @@ export function createChecker(program: Program): Checker { } pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = getTypeForNode(node.value, mapper); + const type = getTypeOrValueForNode(node.value, mapper); linkType(links, type, mapper); pendingResolutions.finish(aliasSymId, ResolutionKind.Type); diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index f9ccb20b0d..1cd32c6649 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -417,6 +417,12 @@ const diagnostics = { withDetails: paramMessage`Type '${"sourceType"}' is not assignable to type '${"targetType"}'\n ${"details"}`, }, }, + "value-in-type": { + severity: "error", + messages: { + default: "A value cannot be used as a type.", + }, + }, "no-prop": { severity: "error", messages: { diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index f1ff872ab8..dc44e15998 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1,9 +1,18 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Diagnostic, FunctionParameterNode, Model, Type } from "../../src/core/index.js"; +import { + DecoratorContext, + Diagnostic, + DiagnosticTarget, + FunctionParameterNode, + Model, + Type, + ValueType, +} from "../../src/core/index.js"; import { BasicTestRunner, DiagnosticMatch, + TestHost, createTestHost, createTestWrapper, expectDiagnosticEmpty, @@ -19,9 +28,9 @@ interface RelatedTypeOptions { describe("compiler: checker: type relations", () => { let runner: BasicTestRunner; + let host: TestHost; beforeEach(async () => { - const host = await createTestHost(); - host.addJsFile("mock.js", { $mock: () => null }); + host = await createTestHost(); runner = createTestWrapper(host); }); @@ -30,6 +39,9 @@ describe("compiler: checker: type relations", () => { diagnostics: readonly Diagnostic[]; expectedDiagnosticPos: number; }> { + host.addJsFile("mock.js", { + $mock: () => null, + }); const { source: code, pos } = extractCursor(` import "./mock.js"; ${commonCode ?? ""} @@ -50,6 +62,41 @@ describe("compiler: checker: type relations", () => { return { related, diagnostics, expectedDiagnosticPos: pos }; } + async function checkValueAssignable({ source, target, commonCode }: RelatedTypeOptions): Promise<{ + related: boolean; + diagnostics: readonly Diagnostic[]; + expectedDiagnosticPos: number; + }> { + let sourceProp: [Type | ValueType, DiagnosticTarget] | undefined; + host.addJsFile("mock.js", { + $mockTarget: () => null, + $mockSource: (context: DecoratorContext, target: Type, x: any) => + (sourceProp = [x, context.getArgumentTarget(0)!]), + }); + const { source: code, pos } = extractCursor(` + import "./mock.js"; + ${commonCode ?? ""} + extern dec mockTarget(target: unknown, target: ${target}); + extern dec mockSource(target: unknown, source: unknown); + + @mockSource(┆${source}) + @test model Test {} + `); + await runner.compile(code); + ok(sourceProp, `Could not find source type for ${source}`); + const decDeclaration = runner.program + .getGlobalNamespaceType() + .decoratorDeclarations.get("mockTarget"); + const targetProp = decDeclaration?.parameters[0].type!; + + const [related, diagnostics] = runner.program.checker.isTypeAssignableTo( + sourceProp[0], + targetProp, + sourceProp[1] + ); + return { related, diagnostics, expectedDiagnosticPos: pos }; + } + async function expectTypeAssignable(options: RelatedTypeOptions) { const { related, diagnostics } = await checkTypeAssignable(options); expectDiagnosticEmpty(diagnostics); @@ -62,6 +109,18 @@ describe("compiler: checker: type relations", () => { expectDiagnostics(diagnostics, { ...match, pos: expectedDiagnosticPos }); } + async function expectValueAssignable(options: RelatedTypeOptions) { + const { related, diagnostics } = await checkValueAssignable(options); + expectDiagnosticEmpty(diagnostics); + ok(related, `Value ${options.source} should be assignable to ${options.target}`); + } + + async function expectValueNotAssignable(options: RelatedTypeOptions, match: DiagnosticMatch) { + const { related, diagnostics, expectedDiagnosticPos } = await checkValueAssignable(options); + ok(!related, `Value ${options.source} should NOT be assignable to ${options.target}`); + expectDiagnostics(diagnostics, { ...match, pos: expectedDiagnosticPos }); + } + describe("model with indexer", () => { it("can add property of subtype of indexer", async () => { const diagnostics = await runner.diagnose(` @@ -1197,7 +1256,7 @@ describe("compiler: checker: type relations", () => { describe("valueof model", () => { it("can assign object literal", async () => { - await expectTypeAssignable({ + await expectValueAssignable({ source: `#{name: "foo"}`, target: "valueof Info", commonCode: `model Info { name: string }`, @@ -1205,7 +1264,7 @@ describe("compiler: checker: type relations", () => { }); it("can assign object literal with optional properties", async () => { - await expectTypeAssignable({ + await expectValueAssignable({ source: `#{name: "foo"}`, target: "valueof Info", commonCode: `model Info { name: string, age?: int32 }`, @@ -1227,7 +1286,7 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign a tuple literal", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `#["foo"]`, target: "valueof Info", @@ -1253,14 +1312,14 @@ describe("compiler: checker: type relations", () => { describe("valueof array", () => { it("can assign tuple literal", async () => { - await expectTypeAssignable({ + await expectValueAssignable({ source: `#["foo"]`, target: "valueof string[]", }); }); it("can assign tuple literal of object literal", async () => { - await expectTypeAssignable({ + await expectValueAssignable({ source: `#[#{name: "a"}, #{name: "b"}]`, target: "valueof Info[]", commonCode: `model Info { name: string }`, @@ -1268,7 +1327,7 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign a tuple", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `["foo"]`, target: "valueof string[]", @@ -1281,7 +1340,7 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign an object literal", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `#{name: "foo"}`, target: "valueof string[]", From ce94cd811367f809789f3f9f58fddc2b23801dbd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 13:56:37 -0700 Subject: [PATCH 016/184] Add test to make sure value not allowed in certain usages --- ...object-literals.test.ts => values.test.ts} | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) rename packages/compiler/test/checker/{object-literals.test.ts => values.test.ts} (78%) diff --git a/packages/compiler/test/checker/object-literals.test.ts b/packages/compiler/test/checker/values.test.ts similarity index 78% rename from packages/compiler/test/checker/object-literals.test.ts rename to packages/compiler/test/checker/values.test.ts index 0b0805d6b8..03a4e224fd 100644 --- a/packages/compiler/test/checker/object-literals.test.ts +++ b/packages/compiler/test/checker/values.test.ts @@ -3,10 +3,21 @@ import { describe, it } from "vitest"; import { Diagnostic, Type } from "../../src/index.js"; import { createTestHost, + createTestRunner, expectDiagnosticEmpty, expectDiagnostics, + extractCursor, } from "../../src/testing/index.js"; +async function diagnoseUsage( + code: string +): Promise<{ diagnostics: readonly Diagnostic[]; pos: number }> { + const runner = await createTestRunner(); + const { source, pos } = extractCursor(code); + const diagnostics = await runner.diagnose(source); + return { diagnostics, pos }; +} + async function compileAndDiagnoseValueType( code: string, other?: string @@ -152,6 +163,32 @@ describe("object literals", () => { message: "Type must be a literal type.", }); }); + + describe("emit diagnostic when used in", () => { + it("emit diagnostic when used in a model", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test { + prop: ┆#{ name: "John" }; + } + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + + it("emit diagnostic when used in template constraint", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + }); }); describe("tuple literals", () => { @@ -206,4 +243,30 @@ describe("tuple literals", () => { message: "Type must be a literal type.", }); }); + + describe("emit diagnostic when used in", () => { + it("emit diagnostic when used in a model", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test { + prop: ┆#["John"]; + } + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + + it("emit diagnostic when used in template constraint", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + }); }); From 8f3296486d469eb8c1fceb6e57ff6c74f3519981 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 14:04:10 -0700 Subject: [PATCH 017/184] Fix --- packages/compiler/src/core/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5d766d1761..10db7e3702 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -835,7 +835,7 @@ export function createChecker(program: Program): Checker { constraint: Type | ValueType | undefined ) { function visit(node: Node) { - const type = getTypeForNode(node); + const type = getTypeOrValueForNode(node); let hasError = false; if (type.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { From a1cc33febc3f4bd6981a4e7ab7da7fe920a101dd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 14:36:21 -0700 Subject: [PATCH 018/184] . --- packages/compiler/src/core/checker.ts | 13 ++++++++++--- packages/http/lib/auth.tsp | 2 +- packages/http/test/http-decorators.test.ts | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 10db7e3702..3016f61cd6 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3685,11 +3685,18 @@ export function createChecker(program: Program): Checker { const [valid] = isStringTemplateSerializable(type); return valid; } + const valueTypes = new Set(["String", "Number", "Boolean", "ObjectLiteral", "TupleLiteral"]); + return valueTypes.has(type.kind); + } + + function isDefaultValue(type: Type): boolean { if (type.kind === "UnionVariant") { return isValueType(type.type); } - const valueTypes = new Set(["String", "Number", "Boolean", "ObjectLiteral", "TupleLiteral"]); - return valueTypes.has(type.kind); + if (type.kind === "EnumMember") { + return true; + } + return isValueType(type); } function checkDefault(defaultNode: Node, type: Type): Type { @@ -3697,7 +3704,7 @@ export function createChecker(program: Program): Checker { if (isErrorType(type)) { return errorType; } - if (!isValueType(defaultType)) { + if (!isDefaultValue(defaultType)) { reportCheckerDiagnostic( createDiagnostic({ code: "unsupported-default", diff --git a/packages/http/lib/auth.tsp b/packages/http/lib/auth.tsp index b142ffaee4..1cf81dd2a3 100644 --- a/packages/http/lib/auth.tsp +++ b/packages/http/lib/auth.tsp @@ -111,7 +111,7 @@ model ApiKeyAuth { * @template Scopes The list of OAuth2 scopes, which are common for every flow from `Flows`. This list is combined with the scopes defined in specific OAuth2 flows. */ @doc("") -model OAuth2Auth { +model OAuth2Auth { @doc("OAuth2 authentication") type: AuthType.oauth2; diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index ab292519c7..590814b9fb 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -796,7 +796,7 @@ describe("http: decorators", () => { it("can specify OAuth2 with scopes, which are default for every flow", async () => { const { Foo } = (await runner.compile(` - alias MyAuth = OAuth2Auth = OAuth2Auth Date: Fri, 15 Mar 2024 14:55:30 -0700 Subject: [PATCH 019/184] Fix openapi3 emitter --- packages/openapi3/src/schema-emitter.ts | 1 + packages/openapi3/test/array.test.ts | 12 ++++++------ packages/openapi3/test/security.test.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 5b579c6dee..0adc0e62c3 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -958,6 +958,7 @@ export function getDefaultValue(program: Program, type: Type, defaultType: Type) case "Boolean": return defaultType.value; case "Tuple": + case "TupleLiteral": compilerAssert( type.kind === "Tuple" || (type.kind === "Model" && isArrayModelType(program, type)), "setting tuple default to non-tuple value" diff --git a/packages/openapi3/test/array.test.ts b/packages/openapi3/test/array.test.ts index c31e5554d4..6e5a190520 100644 --- a/packages/openapi3/test/array.test.ts +++ b/packages/openapi3/test/array.test.ts @@ -155,9 +155,9 @@ describe("openapi3: Array", () => { "Pet", ` model Pet { - names: string[] = ["bismarck"]; - decimals: decimal[] = [123, 456.7]; - decimal128s: decimal128[] = [123, 456.7]; + names: string[] = #["bismarck"]; + decimals: decimal[] = #[123, 456.7]; + decimal128s: decimal128[] = #[123, 456.7]; }; ` ); @@ -198,9 +198,9 @@ describe("openapi3: Array", () => { "Pet", ` model Pet { - names: [string, int32] = ["bismarck", 12]; - decimals: [string, decimal] = ["hi", 456.7]; - decimal128s: [string, decimal128] = ["hi", 456.7]; + names: [string, int32] = #["bismarck", 12]; + decimals: [string, decimal] = #["hi", 456.7]; + decimal128s: [string, decimal128] = #["hi", 456.7]; }; ` ); diff --git a/packages/openapi3/test/security.test.ts b/packages/openapi3/test/security.test.ts index b9f869361b..52143fccca 100644 --- a/packages/openapi3/test/security.test.ts +++ b/packages/openapi3/test/security.test.ts @@ -304,7 +304,7 @@ describe("openapi3: security", () => { ` namespace Test; - alias MyOauth = OAuth2Auth = OAuth2Auth Date: Fri, 15 Mar 2024 15:08:30 -0700 Subject: [PATCH 020/184] Create feature-object-literals-2024-2-15-21-56-46.md --- .../changes/feature-object-literals-2024-2-15-21-56-46.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/feature-object-literals-2024-2-15-21-56-46.md diff --git a/.chronus/changes/feature-object-literals-2024-2-15-21-56-46.md b/.chronus/changes/feature-object-literals-2024-2-15-21-56-46.md new file mode 100644 index 0000000000..c803cf6819 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-2-15-21-56-46.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/openapi3" +--- + +Add support for tuple literal in default values From 7cb98d591e5b4548899c8c26764f1ef84d4c5d13 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 15:11:54 -0700 Subject: [PATCH 021/184] Add quickfix --- packages/compiler/src/core/checker.ts | 15 +++++++++ .../compiler-code-fixes/tuple-to-literal.ts | 16 ++++++++++ .../tuple-to-literal.codefix.test.ts | 23 ++++++++++++++ packages/openapi3/test/array.test.ts | 31 +++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.ts create mode 100644 packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3016f61cd6..6f769d56dc 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -9,6 +9,7 @@ import { } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; +import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; @@ -3696,6 +3697,20 @@ export function createChecker(program: Program): Checker { if (type.kind === "EnumMember") { return true; } + if (type.kind === "Tuple") { + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createTupleToLiteralCodeFix(type.node)], + format: { + message: + "Using a tuple as a default value is deprecated. Use a tuple literal instead. `#[]`", + }, + target: type.node, + }) + ); + return true; + } return isValueType(type); } diff --git a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.ts b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.ts new file mode 100644 index 0000000000..bad5e26d8d --- /dev/null +++ b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.ts @@ -0,0 +1,16 @@ +import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; +import type { TupleExpressionNode } from "../types.js"; + +/** + * Quick fix that convert a tuple to a tuple literal. + */ +export function createTupleToLiteralCodeFix(node: TupleExpressionNode) { + return defineCodeFix({ + id: "tuple-to-literal", + label: `Convert to a tuple literal \`#[]\``, + fix: (context) => { + const location = getSourceLocation(node); + return context.prependText(location, "#"); + }, + }); +} diff --git a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts new file mode 100644 index 0000000000..7d0c52ef5e --- /dev/null +++ b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts @@ -0,0 +1,23 @@ +import { strictEqual } from "assert"; +import { it } from "vitest"; +import { createTupleToLiteralCodeFix } from "../../../src/core/compiler-code-fixes/tuple-to-literal.js"; +import { SyntaxKind } from "../../../src/index.js"; +import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; + +it("it change tuple to a tuple literal", async () => { + await expectCodeFixOnAst( + ` + model Foo { + a: string[] = ┆["abc"]; + } + `, + (node) => { + strictEqual(node.kind, SyntaxKind.TupleExpression); + return createTupleToLiteralCodeFix(node); + } + ).toChangeTo(` + model Foo { + a: string[] = #["abc"]; + } + `); +}); diff --git a/packages/openapi3/test/array.test.ts b/packages/openapi3/test/array.test.ts index 6e5a190520..4be91020b6 100644 --- a/packages/openapi3/test/array.test.ts +++ b/packages/openapi3/test/array.test.ts @@ -181,6 +181,37 @@ describe("openapi3: Array", () => { }); }); + it("can specify array defaults using tuple syntax (LEGACY)", async () => { + const res = await oapiForModel( + "Pet", + ` + model Pet { + names: string[] = ["bismarck"]; + decimals: decimal[] = [123, 456.7]; + decimal128s: decimal128[] = [123, 456.7]; + }; + ` + ); + + deepStrictEqual(res.schemas.Pet.properties.names, { + type: "array", + items: { type: "string" }, + default: ["bismarck"], + }); + + deepStrictEqual(res.schemas.Pet.properties.decimals, { + type: "array", + items: { type: "number", format: "decimal" }, + default: [123, 456.7], + }); + + deepStrictEqual(res.schemas.Pet.properties.decimal128s, { + type: "array", + items: { type: "number", format: "decimal128" }, + default: [123, 456.7], + }); + }); + it("supports summary", async () => { const res = await oapiForModel( "Foo", From 89a7811aa04b9817ae0c61c1ceed588984397473 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 15:17:28 -0700 Subject: [PATCH 022/184] Fix filename --- packages/compiler/src/core/checker.ts | 2 +- .../{tuple-to-literal.ts => tuple-to-literal.codefix.ts} | 0 .../core/compiler-code-fixes/tuple-to-literal.codefix.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/compiler/src/core/compiler-code-fixes/{tuple-to-literal.ts => tuple-to-literal.codefix.ts} (100%) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 6f769d56dc..accb3d0a5b 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -9,7 +9,7 @@ import { } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; -import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.js"; +import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; diff --git a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.ts b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts similarity index 100% rename from packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.ts rename to packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts diff --git a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts index 7d0c52ef5e..f52be771b2 100644 --- a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts +++ b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts @@ -1,6 +1,6 @@ import { strictEqual } from "assert"; import { it } from "vitest"; -import { createTupleToLiteralCodeFix } from "../../../src/core/compiler-code-fixes/tuple-to-literal.js"; +import { createTupleToLiteralCodeFix } from "../../../src/core/compiler-code-fixes/tuple-to-literal.codefix.js"; import { SyntaxKind } from "../../../src/index.js"; import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; From 377f8b66fe117421b491d87869ee767be175c665 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 15:21:39 -0700 Subject: [PATCH 023/184] Add codefix to convert model expression to object literal --- .../model-to-literal.codefix.ts | 16 +++++++++++++ .../model-to-literal.codefix.test.ts | 23 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts create mode 100644 packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts diff --git a/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts new file mode 100644 index 0000000000..d444614c32 --- /dev/null +++ b/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts @@ -0,0 +1,16 @@ +import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; +import type { ModelExpressionNode } from "../types.js"; + +/** + * Quick fix that convert a model expression to an object literal. + */ +export function createModelToLiteralCodeFix(node: ModelExpressionNode) { + return defineCodeFix({ + id: "model-to-literal", + label: `Convert to an object literal \`#{}\``, + fix: (context) => { + const location = getSourceLocation(node); + return context.prependText(location, "#"); + }, + }); +} diff --git a/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts new file mode 100644 index 0000000000..19b772a859 --- /dev/null +++ b/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts @@ -0,0 +1,23 @@ +import { strictEqual } from "assert"; +import { it } from "vitest"; +import { createModelToLiteralCodeFix } from "../../../src/core/compiler-code-fixes/model-to-literal.codefix.js"; +import { SyntaxKind } from "../../../src/index.js"; +import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; + +it("it change model expression to an object literal", async () => { + await expectCodeFixOnAst( + ` + model Foo { + a: string[] = ┆{foo: "abc"}; + } + `, + (node) => { + strictEqual(node.kind, SyntaxKind.ModelExpression); + return createModelToLiteralCodeFix(node); + } + ).toChangeTo(` + model Foo { + a: string[] = #{foo: "abc"}; + } + `); +}); From f387e1e71197187a05c9d3421d55f371253eeabe Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 15:44:37 -0700 Subject: [PATCH 024/184] Fix --- packages/openapi3/test/array.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/openapi3/test/array.test.ts b/packages/openapi3/test/array.test.ts index 4be91020b6..fd4e3ffa51 100644 --- a/packages/openapi3/test/array.test.ts +++ b/packages/openapi3/test/array.test.ts @@ -186,8 +186,11 @@ describe("openapi3: Array", () => { "Pet", ` model Pet { + #suppress "deprecated" "for testing" names: string[] = ["bismarck"]; + #suppress "deprecated" "for testing" decimals: decimal[] = [123, 456.7]; + #suppress "deprecated" "for testing" decimal128s: decimal128[] = [123, 456.7]; }; ` From 7bec3abec543c19f1d4fa761aa0bde8ae0d9db57 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 19:38:40 -0700 Subject: [PATCH 025/184] . --- docs/language-basics/type-and-values.md | 49 +++++++++++++++++++++++++ packages/website/sidebars.ts | 1 + 2 files changed, 50 insertions(+) create mode 100644 docs/language-basics/type-and-values.md diff --git a/docs/language-basics/type-and-values.md b/docs/language-basics/type-and-values.md new file mode 100644 index 0000000000..5419b112d5 --- /dev/null +++ b/docs/language-basics/type-and-values.md @@ -0,0 +1,49 @@ +--- +id: "type-and-values" +title: "Type and Values" +--- + +# Type and Values in TypeSpec + +TypeSpec has the concept of Types and Values, entities can be either a Type, a Value or both depending on the context. + +| Entity Name | Type | Value | +| ---------------- | ---- | ----- | +| `Namespace` | ✅ | | +| `Model` | ✅ | | +| `ModelProperty` | ✅ | | +| `Union` | ✅ | | +| `UnionVariant` | ✅ | | +| `Interface` | ✅ | | +| `Operation` | ✅ | | +| `Scalar` | ✅ | | +| `Tuple` | ✅ | | +| `Enum` | ✅ | | +| `EnumMember` | ✅ | ✅ | +| `EnumMember` | ✅ | ✅ | +| `StringLiteral` | ✅ | ✅ | +| `NumberLiteral` | ✅ | ✅ | +| `BooleanLiteral` | ✅ | ✅ | +| `ObjectLiteral` | | ✅ | +| `TupleLiteral` | | ✅ | + +## Contexts + +There is 3 context that can exists in TypeSpec: + +- **Type only** This is when an expression can only be a Type. + - Model property type + - Array element type + - Tuple values + - Operation parameters + - Operation return type + - Union variant type with some exceptions when used as a decorator or template parameter constraint. +- **Value only** This is when an expression can only be a Value. + - Default values +- **Type and Value Constaints** This is when an expression can be a type or a `valueof` + - Decorator parameters + - Template parameters +- **Type and Value** This is when an expression can be a type or a value. + - Aliases + - Decorator arguments + - Template arguments diff --git a/packages/website/sidebars.ts b/packages/website/sidebars.ts index c59041518e..2dca7bbe2c 100644 --- a/packages/website/sidebars.ts +++ b/packages/website/sidebars.ts @@ -96,6 +96,7 @@ const sidebars: SidebarsConfig = { "language-basics/type-literals", "language-basics/aliases", "language-basics/type-relations", + "language-basics/type-and-values", ], }, { From 17382408abe513b36b4c6845610268ffb39b7358 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 20:16:28 -0700 Subject: [PATCH 026/184] Unify Values and allow enum member as values --- docs/language-basics/type-and-values.md | 41 +++++++------- packages/compiler/src/core/checker.ts | 56 +++++++------------ packages/compiler/src/core/messages.ts | 4 +- packages/compiler/src/core/type-utils.ts | 23 ++++++++ packages/compiler/src/core/types.ts | 14 ++++- packages/compiler/test/checker/values.test.ts | 18 +++--- 6 files changed, 89 insertions(+), 67 deletions(-) diff --git a/docs/language-basics/type-and-values.md b/docs/language-basics/type-and-values.md index 5419b112d5..6a0597a05d 100644 --- a/docs/language-basics/type-and-values.md +++ b/docs/language-basics/type-and-values.md @@ -7,25 +7,28 @@ title: "Type and Values" TypeSpec has the concept of Types and Values, entities can be either a Type, a Value or both depending on the context. -| Entity Name | Type | Value | -| ---------------- | ---- | ----- | -| `Namespace` | ✅ | | -| `Model` | ✅ | | -| `ModelProperty` | ✅ | | -| `Union` | ✅ | | -| `UnionVariant` | ✅ | | -| `Interface` | ✅ | | -| `Operation` | ✅ | | -| `Scalar` | ✅ | | -| `Tuple` | ✅ | | -| `Enum` | ✅ | | -| `EnumMember` | ✅ | ✅ | -| `EnumMember` | ✅ | ✅ | -| `StringLiteral` | ✅ | ✅ | -| `NumberLiteral` | ✅ | ✅ | -| `BooleanLiteral` | ✅ | ✅ | -| `ObjectLiteral` | | ✅ | -| `TupleLiteral` | | ✅ | +| Entity Name | Type | Value | +| -------------------- | ---- | ----- | +| `Namespace` | ✅ | | +| `Model` | ✅ | | +| `ModelProperty` | ✅ | | +| `Union` | ✅ | | +| `UnionVariant` | ✅ | | +| `Interface` | ✅ | | +| `Operation` | ✅ | | +| `Scalar` | ✅ | | +| `Tuple` | ✅ | | +| `Enum` | ✅ | | +| `EnumMember` | ✅ | ✅ | +| `EnumMember` | ✅ | ✅ | +| `StringLiteral` | ✅ | ✅ | +| `NumberLiteral` | ✅ | ✅ | +| `BooleanLiteral` | ✅ | ✅ | +| `ObjectLiteral` | | ✅ | +| `TupleLiteral` | | ✅ | +| ---- _intrinsic_ --- | --- | --- | +| `null` | ✅ | ✅ | +| `unknown` | ✅ | | ## Contexts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index accb3d0a5b..a79e9b16c4 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -14,7 +14,6 @@ import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { TypeNameOptions, getNamespaceFullName, getTypeName } from "./helpers/index.js"; -import { isStringTemplateSerializable } from "./helpers/string-template-utils.js"; import { marshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { @@ -31,6 +30,7 @@ import { isNeverType, isTemplateInstance, isUnknownType, + isValueType, isVoidType, } from "./type-utils.js"; import { @@ -156,6 +156,7 @@ import { UnionVariant, UnionVariantNode, UnknownType, + Value, ValueOfExpressionNode, ValueType, VoidType, @@ -1573,7 +1574,7 @@ export function createChecker(program: Program): Checker { return parameterType; } - function getTypeOrValueForNode(node: Node, mapper?: TypeMapper) { + function getTypeOrValueForNode(node: Node, mapper?: TypeMapper): Type | Value { switch (node.kind) { case SyntaxKind.ObjectLiteral: return checkObjectLiteral(node, mapper); @@ -1584,7 +1585,7 @@ export function createChecker(program: Program): Checker { } } - function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper) { + function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): Type | ValueType { if (node.kind === SyntaxKind.ValueOfExpression) { return checkValueOfExpression(node, mapper); } @@ -2972,7 +2973,10 @@ export function createChecker(program: Program): Checker { } } - function checkObjectLiteral(node: ObjectLiteralNode, mapper: TypeMapper | undefined) { + function checkObjectLiteral( + node: ObjectLiteralNode, + mapper: TypeMapper | undefined + ): ObjectLiteral { return createAndFinishType({ kind: "ObjectLiteral", node: node, @@ -2983,13 +2987,13 @@ export function createChecker(program: Program): Checker { function checkObjectLiteralProperties( node: ObjectLiteralNode, mapper: TypeMapper | undefined - ): Map { - const properties = new Map(); + ): Map { + const properties = new Map(); for (const prop of node.properties!) { if ("id" in prop) { const type = getTypeOrValueForNode(prop.value, mapper); - if (checkIsLiteralType(type, prop.value)) { + if (checkIsValue(type, prop.value)) { properties.set(prop.id.sv, type); } } else { @@ -3004,18 +3008,15 @@ export function createChecker(program: Program): Checker { return properties; } - function checkIsLiteralType( - type: Type, - diagnosticTarget: DiagnosticTarget - ): type is LiteralType | ObjectLiteral | TupleLiteral { - if ( - type.kind !== "ObjectLiteral" && - type.kind !== "TupleLiteral" && - type.kind !== "String" && - type.kind !== "Number" && - type.kind !== "Boolean" - ) { - reportCheckerDiagnostic(createDiagnostic({ code: "not-literal", target: diagnosticTarget })); + function checkIsValue(type: Type, diagnosticTarget: DiagnosticTarget): type is Value { + if (!isValueType(type)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(type) }, + target: diagnosticTarget, + }) + ); return false; } return true; @@ -3042,7 +3043,7 @@ export function createChecker(program: Program): Checker { const values = node.values .map((itemNode) => { const type = getTypeOrValueForNode(itemNode, mapper); - if (checkIsLiteralType(type, itemNode)) { + if (checkIsValue(type, itemNode)) { return type; } else { return undefined; // TODO: do we want to omit this or include an error type? @@ -3678,25 +3679,10 @@ export function createChecker(program: Program): Checker { }; } - function isValueType(type: Type): boolean { - if (type === nullType) { - return true; - } - if (type.kind === "StringTemplate") { - const [valid] = isStringTemplateSerializable(type); - return valid; - } - const valueTypes = new Set(["String", "Number", "Boolean", "ObjectLiteral", "TupleLiteral"]); - return valueTypes.has(type.kind); - } - function isDefaultValue(type: Type): boolean { if (type.kind === "UnionVariant") { return isValueType(type.type); } - if (type.kind === "EnumMember") { - return true; - } if (type.kind === "Tuple") { reportCheckerDiagnostic( createDiagnostic({ diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 1cd32c6649..2300abdd2a 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -404,10 +404,10 @@ const diagnostics = { default: "Cannot spread properties of non-object type.", }, }, - "not-literal": { + "expect-value": { severity: "error", messages: { - default: "Type must be a literal type.", // TODO: better message? Goal here is to say you needed to use another literal type inside a literal object or tuple. + default: paramMessage`${"name"} refers to a type, but is being used as a value here.`, }, }, unassignable: { diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index 046693200a..b15f0b5494 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -1,3 +1,4 @@ +import { isStringTemplateSerializable } from "./helpers/string-template-utils.js"; import { Program } from "./program.js"; import { Enum, @@ -17,6 +18,7 @@ import { Type, TypeMapper, UnknownType, + Value, VoidType, } from "./types.js"; @@ -40,6 +42,27 @@ export function isNullType(type: Type): type is NullType { return type.kind === "Intrinsic" && type.name === "null"; } +const valueTypes = new Set([ + "String", + "Number", + "Boolean", + "EnumMember", + "ObjectLiteral", + "TupleLiteral", +]); + +export function isValueType(type: Type): type is Value { + if (isNullType(type)) { + return true; + } + if (type.kind === "StringTemplate") { + const [valid] = isStringTemplateSerializable(type); + return valid; + } + + return valueTypes.has(type.kind); +} + /** * Lookup and find the node * @param node Node diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index d1fddeebb8..bdf3b7e5d1 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -115,6 +115,14 @@ export type Type = | TupleLiteral | Projection; +export type Value = + | StringLiteral + | NumericLiteral + | BooleanLiteral + | ObjectLiteral + | TupleLiteral + | EnumMember; + export type StdTypes = { // Models Array: Model; @@ -153,7 +161,7 @@ export interface Projector { } export interface ValueType { - kind: "Value"; // Todo remove? + kind: "Value"; target: Type; } @@ -295,12 +303,12 @@ export interface ModelProperty extends BaseType, DecoratedType { export interface ObjectLiteral extends BaseType { kind: "ObjectLiteral"; - properties: Map; + properties: Map; } export interface TupleLiteral extends BaseType { kind: "TupleLiteral"; - values: (LiteralType | ObjectLiteral | TupleLiteral)[]; + values: Value[]; } export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { diff --git a/packages/compiler/test/checker/values.test.ts b/packages/compiler/test/checker/values.test.ts index 03a4e224fd..557558b2a2 100644 --- a/packages/compiler/test/checker/values.test.ts +++ b/packages/compiler/test/checker/values.test.ts @@ -146,10 +146,11 @@ describe("object literals", () => { ["String", `"John"`], ["Number", "21"], ["Boolean", "true"], + ["EnumMember", "Direction.up", "enum Direction { up, down }"], ["ObjectLiteral", `#{nested: "foo"}`], ["TupleLiteral", `#["foo"]`], - ])("%s", async (kind, type) => { - const object = await compileValueType(`#{prop: ${type}}`); + ])("%s", async (kind, type, other?) => { + const object = await compileValueType(`#{prop: ${type}}`, other); strictEqual(object.kind, "ObjectLiteral"); const nameProp = object.properties.get("prop"); strictEqual(nameProp?.kind, kind); @@ -159,8 +160,8 @@ describe("object literals", () => { it("emit diagnostic if referencing a non literal type", async () => { const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); expectDiagnostics(diagnostics, { - code: "not-literal", - message: "Type must be a literal type.", + code: "expect-value", + message: "(anonymous model) refers to a type, but is being used as a value here.", }); }); @@ -226,10 +227,11 @@ describe("tuple literals", () => { ["String", `"John"`], ["Number", "21"], ["Boolean", "true"], + ["EnumMember", "Direction.up", "enum Direction { up, down }"], ["ObjectLiteral", `#{nested: "foo"}`], ["TupleLiteral", `#["foo"]`], - ])("%s", async (kind, type) => { - const object = await compileValueType(`#[${type}]`); + ])("%s", async (kind, type, other?) => { + const object = await compileValueType(`#[${type}]`, other); strictEqual(object.kind, "TupleLiteral"); const nameProp = object.values[0]; strictEqual(nameProp?.kind, kind); @@ -239,8 +241,8 @@ describe("tuple literals", () => { it("emit diagnostic if referencing a non literal type", async () => { const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); expectDiagnostics(diagnostics, { - code: "not-literal", - message: "Type must be a literal type.", + code: "expect-value", + message: "(anonymous model) refers to a type, but is being used as a value here.", }); }); From deced7a6a86c1cced921808f88f65f44426af31b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 20:45:38 -0700 Subject: [PATCH 027/184] . --- packages/samples/specs/authentication/operation-auth.tsp | 2 +- packages/samples/specs/optional/optional.tsp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/samples/specs/authentication/operation-auth.tsp b/packages/samples/specs/authentication/operation-auth.tsp index 45f0f5d154..21d7dd448c 100644 --- a/packages/samples/specs/authentication/operation-auth.tsp +++ b/packages/samples/specs/authentication/operation-auth.tsp @@ -8,7 +8,7 @@ using TypeSpec.Http; @useAuth(BearerAuth | MyAuth<["read", "write"]>) namespace TypeSpec.OperationAuth; -alias MyAuth = OAuth2Auth< +alias MyAuth = OAuth2Auth< Flows = [ { type: OAuth2FlowType.implicit; diff --git a/packages/samples/specs/optional/optional.tsp b/packages/samples/specs/optional/optional.tsp index 879de77ec5..583e70de97 100644 --- a/packages/samples/specs/optional/optional.tsp +++ b/packages/samples/specs/optional/optional.tsp @@ -12,7 +12,7 @@ model HasOptional { optionalString?: string = "default string"; optionalNumber?: int32 = 123; optionalBoolean?: boolean = true; - optionalArray?: string[] = ["foo", "bar"]; + optionalArray?: string[] = #["foo", "bar"]; optionalUnion?: "foo" | "bar" = "foo"; optionalEnum?: MyEnum = MyEnum.a; } From 8129fe417a367d1cff296767ba7226a04c091eab Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 15 Mar 2024 21:10:34 -0700 Subject: [PATCH 028/184] . --- packages/compiler/src/core/checker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index a79e9b16c4..cfb212ff94 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -890,6 +890,7 @@ export function createChecker(program: Program): Checker { } function checkTemplateArgument(node: TemplateArgumentNode, mapper: TypeMapper | undefined): Type { + console.log("A", node.argument); return getTypeOrValueForNode(node.argument, mapper); } @@ -983,7 +984,7 @@ export function createChecker(program: Program): Checker { } const initMap = new Map( decls.map(function (decl) { - const declaredType = getTypeForNode(decl)! as TemplateParameter; + const declaredType = getTypeOrValueForNode(decl)! as TemplateParameter; positional.push(declaredType); params.set(decl.id.sv, declaredType); @@ -1002,7 +1003,7 @@ export function createChecker(program: Program): Checker { for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { function deferredCheck(): [Node, Type] { - return [arg, getTypeForNode(arg.argument, mapper)]; + return [arg, getTypeOrValueForNode(arg.argument, mapper)]; } if (arg.name) { From fd87dc3718fb689125be6318340e38c1f688a40d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 16 Mar 2024 14:40:21 -0700 Subject: [PATCH 029/184] progress --- packages/compiler/src/core/checker.ts | 76 +++++++++------- .../src/core/helpers/type-name-utils.ts | 2 +- packages/compiler/src/core/type-utils.ts | 11 +++ packages/compiler/src/core/types.ts | 6 +- .../compiler/test/checker/relation.test.ts | 86 ++++++++++++++----- 5 files changed, 126 insertions(+), 55 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index cfb212ff94..8019ff353c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -30,6 +30,7 @@ import { isNeverType, isTemplateInstance, isUnknownType, + isValueOnly, isValueType, isVoidType, } from "./type-utils.js"; @@ -255,6 +256,9 @@ export interface Checker { */ resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]]; + /** @internal */ + getTypeOrValueForNode(node: Node): Type; + errorType: ErrorType; voidType: VoidType; neverType: NeverType; @@ -409,6 +413,7 @@ export function createChecker(program: Program): Checker { isStdType, getStdType, resolveTypeReference, + getTypeOrValueForNode, }; const projectionMembers = createProjectionMembers(checker); @@ -890,7 +895,6 @@ export function createChecker(program: Program): Checker { } function checkTemplateArgument(node: TemplateArgumentNode, mapper: TypeMapper | undefined): Type { - console.log("A", node.argument); return getTypeOrValueForNode(node.argument, mapper); } @@ -5617,7 +5621,7 @@ export function createChecker(program: Program): Checker { return isAssignableToValueType(source, target, diagnosticTarget, relationCache); } - if (source.kind === "Value") { + if (source.kind === "Value" || isValueOnly(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } const isSimpleTypeRelated = isSimpleTypeAssignableTo(source, target); @@ -5672,22 +5676,10 @@ export function createChecker(program: Program): Checker { ); } else if (target.kind === "Model" && source.kind === "Model") { return isModelRelatedTo(source, target, diagnosticTarget, relationCache); - } else if ( - target.kind === "Model" && - !isArrayModelType(program, target) && - source.kind === "ObjectLiteral" - ) { - return isObjectLiteralOfModelType(source, target, diagnosticTarget, relationCache); } else if ( target.kind === "Model" && isArrayModelType(program, target) && - source.kind === "TupleLiteral" - ) { - return isTupleLiteralOfArrayType(source, target, diagnosticTarget, relationCache); - } else if ( - target.kind === "Model" && - isArrayModelType(program, target) && - (source.kind === "Tuple" || source.kind === "TupleLiteral") + source.kind === "Tuple" ) { for (const item of source.values) { const [related, diagnostics] = isTypeAssignableToInternal( @@ -5701,10 +5693,7 @@ export function createChecker(program: Program): Checker { } } return [Related.true, []]; - } else if ( - target.kind === "Tuple" && - (source.kind === "Tuple" || source.kind === "TupleLiteral") - ) { + } else if (target.kind === "Tuple" && source.kind === "Tuple") { return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); } else if (target.kind === "Union") { return isAssignableToUnion(source, target, diagnosticTarget, relationCache); @@ -5729,20 +5718,43 @@ export function createChecker(program: Program): Checker { relationCache ); } - const [assignable, diagnostics] = isTypeAssignableToInternal( - source, - target.target, - diagnosticTarget, - relationCache - ); - if (!assignable) { - return [assignable, diagnostics]; - } - if (!isValueType(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - return [Related.true, []]; + + return isValueOfType(source, target.target, diagnosticTarget, relationCache); + } + + /** Check if the value is assignable to the given type. */ + function isValueOfType( + source: Value, + target: Type, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + ): [Related, readonly Diagnostic[]] { + if (isUnknownType(target)) return [Related.true, []]; + + switch (source.kind) { + case "ObjectLiteral": + if (target.kind === "Model" && !isArrayModelType(program, target)) { + return isObjectLiteralOfModelType(source, target, diagnosticTarget, relationCache); + } + break; + case "TupleLiteral": + if (target.kind === "Model" && isArrayModelType(program, target)) { + return isTupleLiteralOfArrayType(source, target, diagnosticTarget, relationCache); + } else if (target.kind === "Tuple") { + return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); + } + break; + case "String": + case "Number": + case "Boolean": + case "EnumMember": + return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); + } + + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } function isReflectionType(type: Type): type is Model & { name: ReflectionTypeName } { @@ -5902,7 +5914,7 @@ export function createChecker(program: Program): Checker { } } else { remainingProperties.delete(prop.name); - const [related, propDiagnostics] = isTypeAssignableToInternal( + const [related, propDiagnostics] = isValueOfType( sourceProperty, prop.type, diagnosticTarget, @@ -5934,7 +5946,7 @@ export function createChecker(program: Program): Checker { ): [Related, readonly Diagnostic[]] { relationCache.set([source, target], Related.maybe); for (const value of source.values) { - const [related, diagnostics] = isTypeAssignableToInternal( + const [related, diagnostics] = isValueOfType( value, target.indexer.value, diagnosticTarget, diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 16ea55d72f..2ef1830caa 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -62,7 +62,7 @@ export function getTypeName(type: Type | ValueType, options?: TypeNameOptions): return `#[${type.values.map((x) => getTypeName(x, options)).join(", ")}]`; } - return "(unnamed type)"; + return `(unnamed type)`; } export function isStdNamespace(namespace: Namespace): boolean { diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index b15f0b5494..e94691cd8b 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -19,6 +19,7 @@ import { TypeMapper, UnknownType, Value, + ValueOnly, VoidType, } from "./types.js"; @@ -63,6 +64,16 @@ export function isValueType(type: Type): type is Value { return valueTypes.has(type.kind); } +export function isValueOnly(type: Type): type is ValueOnly { + switch (type.kind) { + case "ObjectLiteral": + case "TupleLiteral": + return true; + default: + return false; + } +} + /** * Lookup and find the node * @param node Node diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index bdf3b7e5d1..8b04f68309 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -115,13 +115,15 @@ export type Type = | TupleLiteral | Projection; +export type ValueOnly = ObjectLiteral | TupleLiteral; + export type Value = | StringLiteral | NumericLiteral | BooleanLiteral + | EnumMember | ObjectLiteral - | TupleLiteral - | EnumMember; + | TupleLiteral; export type StdTypes = { // Models diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index dc44e15998..5666340d33 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1,13 +1,13 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; +import pc from "picocolors"; import { beforeEach, describe, it } from "vitest"; import { - DecoratorContext, + AliasStatementNode, Diagnostic, - DiagnosticTarget, FunctionParameterNode, Model, + SyntaxKind, Type, - ValueType, } from "../../src/core/index.js"; import { BasicTestRunner, @@ -18,6 +18,7 @@ import { expectDiagnosticEmpty, expectDiagnostics, extractCursor, + resolveVirtualPath, } from "../../src/testing/index.js"; interface RelatedTypeOptions { @@ -67,32 +68,31 @@ describe("compiler: checker: type relations", () => { diagnostics: readonly Diagnostic[]; expectedDiagnosticPos: number; }> { - let sourceProp: [Type | ValueType, DiagnosticTarget] | undefined; - host.addJsFile("mock.js", { - $mockTarget: () => null, - $mockSource: (context: DecoratorContext, target: Type, x: any) => - (sourceProp = [x, context.getArgumentTarget(0)!]), - }); + host.addJsFile("mock.js", { $mock: () => null }); const { source: code, pos } = extractCursor(` import "./mock.js"; ${commonCode ?? ""} - extern dec mockTarget(target: unknown, target: ${target}); - extern dec mockSource(target: unknown, source: unknown); + extern dec mock(target: unknown, target: ${target}); - @mockSource(┆${source}) - @test model Test {} + alias Source = ┆${source}; `); await runner.compile(code); + const alias: AliasStatementNode | undefined = runner.program.sourceFiles + .get(resolveVirtualPath("main.tsp")) + ?.statements.find((x): x is AliasStatementNode => x.kind === SyntaxKind.AliasStatement); + ok(alias); + const sourceProp = runner.program.checker.getTypeOrValueForNode(alias.value); ok(sourceProp, `Could not find source type for ${source}`); const decDeclaration = runner.program .getGlobalNamespaceType() - .decoratorDeclarations.get("mockTarget"); - const targetProp = decDeclaration?.parameters[0].type!; + .decoratorDeclarations.get("mock"); + const targetProp = decDeclaration?.parameters[0].type; + ok(targetProp, `Could not find target type for ${target}`); const [related, diagnostics] = runner.program.checker.isTypeAssignableTo( - sourceProp[0], + sourceProp, targetProp, - sourceProp[1] + alias.value ); return { related, diagnostics, expectedDiagnosticPos: pos }; } @@ -1125,7 +1125,7 @@ describe("compiler: checker: type relations", () => { testReflectionType("UnionVariant", "Foo.a", `union Foo {a: string, b: int32};`); }); - describe("Value target", () => { + describe("Value constraint", () => { describe("valueof string", () => { it("can assign string literal", async () => { await expectTypeAssignable({ source: `"foo bar"`, target: "valueof string" }); @@ -1304,7 +1304,7 @@ describe("compiler: checker: type relations", () => { { source: `string`, target: "valueof Info", commonCode: `model Info { name: string }` }, { code: "unassignable", - message: "Type 'string' is not assignable to type 'Info'", + message: "Type 'string' is not assignable to type 'valueof Info'", } ); }); @@ -1357,7 +1357,7 @@ describe("compiler: checker: type relations", () => { { source: `string`, target: "valueof string[]" }, { code: "unassignable", - message: "Type 'string' is not assignable to type 'string[]'", + message: "Type 'string' is not assignable to type 'valueof string[]'", } ); }); @@ -1372,6 +1372,18 @@ describe("compiler: checker: type relations", () => { expectDiagnosticEmpty(diagnostics); }); + it("can use valueof unknown constraint not assignable to unknown", async () => { + const { source, pos } = extractCursor(` + model A {} + model B is A<┆T> {}`); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type 'valueof unknown' is not assignable to type 'unknown'", + pos, + }); + }); + // BackCompat added May 2023 Sprint: by June 2023 sprint. From this PR: https://github.com/microsoft/typespec/pull/1877 it("BACKCOMPAT: can use valueof in template parameter constraints", async () => { const diagnostics = await runner.diagnose(` @@ -1386,4 +1398,38 @@ describe("compiler: checker: type relations", () => { }); }); }); + + /** Describe the relation between types and values in TypeSpec */ + describe.only("value vs type constraints", () => { + describe("cannot assign a value to a type constraint", () => { + it.each([ + ["#{}", "{}"], + ["#{}", "unknown"], + ["#[]", "unknown[]"], + ["#[]", "unknown"], + ])(`${pc.cyan("%s")} => ${pc.cyan("%s")}`, async (source, target) => { + await expectValueNotAssignable({ source, target }, { code: "unassignable" }); + }); + }); + + describe("cannot assign a type to a value constraint", () => { + it.each([ + ["{}", "valueof unknown"], + ["{}", "valueof {}"], + ])(`${pc.cyan("%s")} => ${pc.cyan("%s")}`, async (source, target) => { + await expectTypeNotAssignable({ source, target }, { code: "unassignable" }); + }); + }); + + describe("can assign types or values when constraint accept both", () => { + it.each([ + ["{}", "(valueof unknown) | unknown"], + ["#{}", "(valueof unknown) | unknown"], + ["{}", "(valueof {}) | {}"], + ["#{}", "(valueof {}) | {}"], + ])(`${pc.cyan("%s")} => ${pc.cyan("%s")}`, async (source, target) => { + await expectValueAssignable({ source, target }); + }); + }); + }); }); From c5004e35cf7eb7a7889bfa3de4679bc79cd5d1ef Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 12:20:44 -0700 Subject: [PATCH 030/184] fix --- packages/compiler/test/checker/relation.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 5666340d33..0c2acfb2cf 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1,5 +1,4 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; -import pc from "picocolors"; import { beforeEach, describe, it } from "vitest"; import { AliasStatementNode, @@ -1407,7 +1406,7 @@ describe("compiler: checker: type relations", () => { ["#{}", "unknown"], ["#[]", "unknown[]"], ["#[]", "unknown"], - ])(`${pc.cyan("%s")} => ${pc.cyan("%s")}`, async (source, target) => { + ])(`%s => %s`, async (source, target) => { await expectValueNotAssignable({ source, target }, { code: "unassignable" }); }); }); @@ -1416,7 +1415,7 @@ describe("compiler: checker: type relations", () => { it.each([ ["{}", "valueof unknown"], ["{}", "valueof {}"], - ])(`${pc.cyan("%s")} => ${pc.cyan("%s")}`, async (source, target) => { + ])(`%s => %s`, async (source, target) => { await expectTypeNotAssignable({ source, target }, { code: "unassignable" }); }); }); @@ -1427,7 +1426,7 @@ describe("compiler: checker: type relations", () => { ["#{}", "(valueof unknown) | unknown"], ["{}", "(valueof {}) | {}"], ["#{}", "(valueof {}) | {}"], - ])(`${pc.cyan("%s")} => ${pc.cyan("%s")}`, async (source, target) => { + ])(`%s => %s`, async (source, target) => { await expectValueAssignable({ source, target }); }); }); From d1310454a32f4dbed70e2d0a300fea733bdba9fd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 14:08:02 -0700 Subject: [PATCH 031/184] Add new ParamConstraintUnion --- packages/compiler/src/core/checker.ts | 176 +++++++++++++----- .../src/core/helpers/type-name-utils.ts | 10 +- packages/compiler/src/core/type-utils.ts | 9 +- packages/compiler/src/core/types.ts | 13 +- packages/compiler/src/server/serverlib.ts | 4 +- .../compiler/src/server/type-signature.ts | 4 +- .../compiler/test/checker/relation.test.ts | 8 +- .../tspd/src/ref-doc/emitters/markdown.ts | 11 +- .../tspd/src/ref-doc/utils/type-signature.ts | 3 +- 9 files changed, 174 insertions(+), 64 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 8019ff353c..88ed2d0af0 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -13,7 +13,12 @@ import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-lite import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; -import { TypeNameOptions, getNamespaceFullName, getTypeName } from "./helpers/index.js"; +import { + TypeNameOptions, + getEntityName, + getNamespaceFullName, + getTypeName, +} from "./helpers/index.js"; import { marshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { @@ -52,6 +57,7 @@ import { Diagnostic, DiagnosticTarget, DocContent, + Entity, Enum, EnumMember, EnumMemberNode, @@ -95,6 +101,7 @@ import { ObjectLiteralNode, Operation, OperationStatementNode, + ParamConstraintUnion, Projection, ProjectionArithmeticExpressionNode, ProjectionBlockExpressionNode, @@ -227,8 +234,8 @@ export interface Checker { * @returns [related, list of diagnostics] */ isTypeAssignableTo( - source: Type | ValueType, - target: Type | ValueType, + source: Entity, + target: Entity, diagnosticTarget: DiagnosticTarget ): [boolean, readonly Diagnostic[]]; @@ -804,7 +811,7 @@ export function createChecker(program: Program): Checker { if (node.constraint) { pendingResolutions.start(getNodeSymId(node), ResolutionKind.Constraint); - type.constraint = getTypeOrValueOfTypeForNode(node.constraint); + type.constraint = getParamConstraintEntityForNode(node.constraint); pendingResolutions.finish(getNodeSymId(node), ResolutionKind.Constraint); } if (node.default) { @@ -839,7 +846,7 @@ export function createChecker(program: Program): Checker { nodeDefault: Expression, templateParameters: readonly TemplateParameterDeclarationNode[], index: number, - constraint: Type | ValueType | undefined + constraint: Type | ParamConstraintUnion | ValueType | undefined ) { function visit(node: Node) { const type = getTypeOrValueForNode(node); @@ -1103,7 +1110,9 @@ export function createChecker(program: Program): Checker { // TODO-TIM check if we expose this below commit( param, - param.constraint?.kind === "Value" ? unknownType : param.constraint ?? unknownType + param.constraint?.kind === "Value" || param.constraint?.kind === "ParamConstraintUnion" + ? unknownType + : param.constraint ?? unknownType ); } @@ -1120,7 +1129,10 @@ export function createChecker(program: Program): Checker { if (!checkTypeAssignable(type, constraint, argNode)) { // TODO-TIM check if we expose this below - const effectiveType = param.constraint?.kind === "Value" ? unknownType : param.constraint; + const effectiveType = + param.constraint?.kind === "Value" || param.constraint.kind === "ParamConstraintUnion" + ? unknownType + : param.constraint; commit(param, effectiveType); continue; @@ -1390,6 +1402,23 @@ export function createChecker(program: Program): Checker { return type; } + /** Check a union expresion used in a parameter constraint, those allow the use of `valueof` as a variant. */ + function checkUnionExpressionAsParamConstraint( + node: UnionExpressionNode, + mapper: TypeMapper | undefined + ): Union | ParamConstraintUnion { + const hasValueOf = node.options.some((x) => x.kind === SyntaxKind.ValueOfExpression); + if (!hasValueOf) { + return checkUnionExpression(node, mapper); + } + + return { + kind: "ParamConstraintUnion", + node, + options: node.options.map((x) => getTypeOrValueOfTypeForNode(x, mapper)), + }; + } + function checkUnionExpression(node: UnionExpressionNode, mapper: TypeMapper | undefined): Union { const unionType: Union = createAndFinishType({ kind: "Union", @@ -1563,7 +1592,7 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "rest-parameter-array", target: node.type }) ); } - const type = node.type ? getTypeOrValueOfTypeForNode(node.type) : unknownType; + const type = node.type ? getParamConstraintEntityForNode(node.type) : unknownType; const parameterType: FunctionParameter = createType({ kind: "FunctionParameter", @@ -1591,10 +1620,24 @@ export function createChecker(program: Program): Checker { } function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): Type | ValueType { - if (node.kind === SyntaxKind.ValueOfExpression) { - return checkValueOfExpression(node, mapper); + switch (node.kind) { + case SyntaxKind.ValueOfExpression: + return checkValueOfExpression(node, mapper); + default: + return getTypeForNode(node, mapper); + } + } + + function getParamConstraintEntityForNode( + node: Node, + mapper?: TypeMapper + ): Type | ParamConstraintUnion | ValueType { + switch (node.kind) { + case SyntaxKind.UnionExpression: + return checkUnionExpressionAsParamConstraint(node, mapper); + default: + return getTypeOrValueOfTypeForNode(node, mapper); } - return getTypeForNode(node, mapper); } function mergeModelTypes( @@ -3805,7 +3848,7 @@ export function createChecker(program: Program): Checker { format: { decorator: declaration.name, to: getTypeName(targetType), - expected: getTypeName(declaration.target.type), + expected: getEntityName(declaration.target.type), }, target: decoratorNode, }) @@ -3847,9 +3890,12 @@ export function createChecker(program: Program): Checker { const resolvedArgs: DecoratorArgument[] = []; for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { - const restType = getIndexType( - parameter.type.kind === "Value" ? parameter.type.target : parameter.type - ); + const restType = + parameter.type.kind === "ParamConstraintUnion" + ? undefined + : getIndexType( + parameter.type.kind === "Value" ? parameter.type.target : parameter.type + ); if (restType) { for (let i = index; i < args.length; i++) { const arg = args[i]; @@ -3893,7 +3939,7 @@ export function createChecker(program: Program): Checker { function checkArgumentAssignable( argumentType: Type, - parameterType: Type | ValueType, + parameterType: Type | ParamConstraintUnion | ValueType, diagnosticTarget: DiagnosticTarget ): boolean { const [valid] = isTypeAssignableTo(argumentType, parameterType, diagnosticTarget); @@ -3903,7 +3949,7 @@ export function createChecker(program: Program): Checker { code: "invalid-argument", format: { value: getTypeName(argumentType), - expected: getTypeName(parameterType), + expected: getEntityName(parameterType), }, target: diagnosticTarget, }) @@ -5534,8 +5580,8 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. */ function checkTypeAssignable( - source: Type | ValueType, - target: Type | ValueType, + source: Type | ParamConstraintUnion | ValueType, + target: Type | ParamConstraintUnion | ValueType, diagnosticTarget: DiagnosticTarget ): boolean { const [related, diagnostics] = isTypeAssignableTo(source, target, diagnosticTarget); @@ -5552,24 +5598,24 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. */ function isTypeAssignableTo( - source: Type | ValueType, - target: Type | ValueType, + source: Type | ParamConstraintUnion | ValueType, + target: Type | ParamConstraintUnion | ValueType, diagnosticTarget: DiagnosticTarget ): [boolean, readonly Diagnostic[]] { const [related, diagnostics] = isTypeAssignableToInternal( source, target, diagnosticTarget, - new MultiKeyMap<[Type | ValueType, Type | ValueType], Related>() + new MultiKeyMap<[Entity, Entity], Related>() ); return [related === Related.true, diagnostics]; } function isTypeAssignableToInternal( - source: Type | ValueType, - target: Type | ValueType, + source: Type | ParamConstraintUnion | ValueType, + target: Type | ParamConstraintUnion | ValueType, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { const cached = relationCache.get([source, target]); if (cached !== undefined) { @@ -5579,17 +5625,17 @@ export function createChecker(program: Program): Checker { source, target, diagnosticTarget, - new MultiKeyMap<[Type | ValueType, Type | ValueType], Related>() + new MultiKeyMap<[Entity, Entity], Related>() ); relationCache.set([source, target], result); return [result, diagnostics]; } function isTypeAssignableToWorker( - source: Type | ValueType, - target: Type | ValueType, + source: Entity, + target: Entity, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { // BACKCOMPAT: Added May 2023 sprint, to be removed by June 2023 sprint if (source.kind === "TemplateParameter" && source.constraint && target.kind === "Value") { @@ -5600,10 +5646,10 @@ export function createChecker(program: Program): Checker { relationCache ); if (assignable) { - const constraint = getTypeName(source.constraint); + const constraint = getEntityName(source.constraint); reportDeprecated( program, - `Template constrainted to '${constraint}' will not be assignable to '${getTypeName( + `Template constrainted to '${constraint}' will not be assignable to '${getEntityName( target )}' in the future. Update the constraint to be 'valueof ${constraint}'`, diagnosticTarget @@ -5620,8 +5666,16 @@ export function createChecker(program: Program): Checker { if (target.kind === "Value") { return isAssignableToValueType(source, target, diagnosticTarget, relationCache); } + if (target.kind === "ParamConstraintUnion") { + return isAssignableToParameterConstraintUnion( + source, + target, + diagnosticTarget, + relationCache + ); + } - if (source.kind === "Value" || isValueOnly(source)) { + if (source.kind === "Value" || source.kind === "ParamConstraintUnion" || isValueOnly(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } const isSimpleTypeRelated = isSimpleTypeAssignableTo(source, target); @@ -5705,10 +5759,10 @@ export function createChecker(program: Program): Checker { } function isAssignableToValueType( - source: Type | ValueType, + source: Type | ParamConstraintUnion | ValueType, target: ValueType, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { if (source.kind === "Value") { return isTypeAssignableToInternal( @@ -5725,12 +5779,42 @@ export function createChecker(program: Program): Checker { return isValueOfType(source, target.target, diagnosticTarget, relationCache); } + function isAssignableToParameterConstraintUnion( + source: Type | ParamConstraintUnion | ValueType, + target: ParamConstraintUnion, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Entity, Entity], Related> + ): [Related, readonly Diagnostic[]] { + if (source.kind === "ParamConstraintUnion") { + for (const option of source.options) { + const [variantAssignable] = isAssignableToParameterConstraintUnion( + option, + target, + diagnosticTarget, + relationCache + ); + if (!variantAssignable) { + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + } + } + return [Related.true, []]; + } + + for (const option of target.options) { + const [related] = isTypeAssignableToInternal(source, option, diagnosticTarget, relationCache); + if (related) { + return [Related.true, []]; + } + } + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + } + /** Check if the value is assignable to the given type. */ function isValueOfType( source: Value, target: Type, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { if (isUnknownType(target)) return [Related.true, []]; @@ -5852,7 +5936,7 @@ export function createChecker(program: Program): Checker { source: Model, target: Model, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, Diagnostic[]] { relationCache.set([source, target], Related.maybe); const diagnostics: Diagnostic[] = []; @@ -5891,7 +5975,7 @@ export function createChecker(program: Program): Checker { source: ObjectLiteral, target: Model, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { relationCache.set([source, target], Related.maybe); const diagnostics: Diagnostic[] = []; @@ -5942,7 +6026,7 @@ export function createChecker(program: Program): Checker { source: TupleLiteral, target: ArrayModelType, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { relationCache.set([source, target], Related.maybe); for (const value of source.values) { @@ -5971,7 +6055,7 @@ export function createChecker(program: Program): Checker { properties: Map, indexerConstaint: Type, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { for (const prop of properties.values()) { const [related, diagnostics] = isTypeAssignableToInternal( @@ -5992,7 +6076,7 @@ export function createChecker(program: Program): Checker { source: Model, target: Model & { indexer: ModelIndexer }, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { // Model expressions should be able to be assigned. if (source.name === "" && target.indexer.key.name !== "integer") { @@ -6030,7 +6114,7 @@ export function createChecker(program: Program): Checker { type: Model, constraintType: Type, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { for (const prop of type.properties.values()) { const [related, diagnostics] = isTypeAssignableToInternal( @@ -6062,7 +6146,7 @@ export function createChecker(program: Program): Checker { source: Tuple | TupleLiteral, target: Tuple, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { if (source.values.length !== target.values.length) { return [ @@ -6100,7 +6184,7 @@ export function createChecker(program: Program): Checker { source: Type, target: Union, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, Diagnostic[]] { if (source.kind === "UnionVariant" && source.union === target) { return [Related.true, []]; @@ -6143,13 +6227,13 @@ export function createChecker(program: Program): Checker { } function createUnassignableDiagnostic( - source: Type | ValueType, - target: Type | ValueType, + source: Entity, + target: Entity, diagnosticTarget: DiagnosticTarget ) { return createDiagnostic({ code: "unassignable", - format: { targetType: getTypeName(target), value: getTypeName(source) }, + format: { targetType: getEntityName(target), value: getEntityName(source) }, target: diagnosticTarget, }); } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 2ef1830caa..7b07c67c18 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -1,6 +1,7 @@ import { printId } from "../../formatter/print/printer.js"; import { isTemplateInstance } from "../type-utils.js"; import { + Entity, Enum, Interface, Model, @@ -11,7 +12,6 @@ import { Scalar, Type, Union, - ValueType, } from "../types.js"; export interface TypeNameOptions { @@ -19,7 +19,11 @@ export interface TypeNameOptions { printable?: boolean; } -export function getTypeName(type: Type | ValueType, options?: TypeNameOptions): string { +export function getTypeName(type: Type, options?: TypeNameOptions): string { + return getEntityName(type, options); +} + +export function getEntityName(type: Entity, options?: TypeNameOptions): string { switch (type.kind) { case "Namespace": return getNamespaceFullName(type, options); @@ -56,6 +60,8 @@ export function getTypeName(type: Type | ValueType, options?: TypeNameOptions): return type.name; case "Value": return `valueof ${getTypeName(type.target, options)}`; + case "ParamConstraintUnion": + return type.options.map((x) => getEntityName(x, options)).join(" | "); case "ObjectLiteral": return `#{${[...type.properties.entries()].map(([name, value]) => `${name}: ${getTypeName(value, options)}`).join(", ")}}`; case "TupleLiteral": diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index e94691cd8b..c25f914e7c 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -1,6 +1,7 @@ import { isStringTemplateSerializable } from "./helpers/string-template-utils.js"; import { Program } from "./program.js"; import { + Entity, Enum, ErrorType, Interface, @@ -35,11 +36,11 @@ export function isNeverType(type: Type): type is NeverType { return type.kind === "Intrinsic" && type.name === "never"; } -export function isUnknownType(type: Type): type is UnknownType { +export function isUnknownType(type: Entity): type is UnknownType { return type.kind === "Intrinsic" && type.name === "unknown"; } -export function isNullType(type: Type): type is NullType { +export function isNullType(type: Entity): type is NullType { return type.kind === "Intrinsic" && type.name === "null"; } @@ -52,8 +53,8 @@ const valueTypes = new Set([ "TupleLiteral", ]); -export function isValueType(type: Type): type is Value { - if (isNullType(type)) { +export function isValueType(type: Entity): type is Value { + if (isNullType(type as any)) { return true; } if (type.kind === "StringTemplate") { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 8b04f68309..e4a662f7f2 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -88,6 +88,8 @@ export interface TemplatedTypeBase { templateNode?: Node; } +export type Entity = Type | Value | ValueType | ParamConstraintUnion; + export type Type = | Model | ModelProperty @@ -533,6 +535,13 @@ export interface Tuple extends BaseType { values: Type[]; } +export interface ParamConstraintUnion { + kind: "ParamConstraintUnion"; // TODO: review naming + node: UnionExpressionNode; + + readonly options: (Type | ValueType)[]; +} + export interface Union extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Union"; name?: string; @@ -570,7 +579,7 @@ export interface UnionVariant extends BaseType, DecoratedType { export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; - constraint?: Type | ValueType; + constraint?: Type | ParamConstraintUnion | ValueType; default?: Type; } @@ -598,7 +607,7 @@ export interface FunctionParameter extends BaseType { kind: "FunctionParameter"; node: FunctionParameterNode; name: string; - type: Type | ValueType; + type: Type | ParamConstraintUnion | ValueType; optional: boolean; rest: boolean; } diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 92f25fcd75..addf52a730 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -49,7 +49,7 @@ import { CharCode, codePointBefore, isIdentifierContinue } from "../core/charcod import { resolveCodeFix } from "../core/code-fixes.js"; import { compilerAssert, getSourceLocation } from "../core/diagnostics.js"; import { formatTypeSpec } from "../core/formatter.js"; -import { getTypeName } from "../core/helpers/type-name-utils.js"; +import { getEntityName, getTypeName } from "../core/helpers/type-name-utils.js"; import { ResolveModuleHost, resolveModule } from "../core/index.js"; import { getPositionBeforeTrivia } from "../core/parser-utils.js"; import { getNodeAtPosition, visitChildren } from "../core/parser.js"; @@ -551,7 +551,7 @@ export function createServer(host: ServerHost): Server { ...type.parameters.map((x) => { const info: ParameterInformation = { // prettier-ignore - label: `${x.rest ? "..." : ""}${x.name}${x.optional ? "?" : ""}: ${getTypeName(x.type)}`, + label: `${x.rest ? "..." : ""}${x.name}${x.optional ? "?" : ""}: ${getEntityName(x.type)}`, }; const doc = parameterDocs.get(x.name); if (doc) { diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index 5516407034..a8d8192a45 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -1,5 +1,5 @@ import { compilerAssert } from "../core/diagnostics.js"; -import { getTypeName, isStdNamespace } from "../core/helpers/type-name-utils.js"; +import { getEntityName, getTypeName, isStdNamespace } from "../core/helpers/type-name-utils.js"; import { Program } from "../core/program.js"; import { getFullyQualifiedSymbolName } from "../core/type-utils.js"; import { @@ -111,7 +111,7 @@ function getOperationSignature(type: Operation) { function getFunctionParameterSignature(parameter: FunctionParameter) { const rest = parameter.rest ? "..." : ""; const optional = parameter.optional ? "?" : ""; - return `${rest}${printId(parameter.name)}${optional}: ${getTypeName(parameter.type)}`; + return `${rest}${printId(parameter.name)}${optional}: ${getEntityName(parameter.type)}`; } function getStringTemplateSignature(stringTemplate: StringTemplate) { diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 0c2acfb2cf..a53938c79d 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1399,7 +1399,7 @@ describe("compiler: checker: type relations", () => { }); /** Describe the relation between types and values in TypeSpec */ - describe.only("value vs type constraints", () => { + describe("value vs type constraints", () => { describe("cannot assign a value to a type constraint", () => { it.each([ ["#{}", "{}"], @@ -1429,6 +1429,12 @@ describe("compiler: checker: type relations", () => { ])(`%s => %s`, async (source, target) => { await expectValueAssignable({ source, target }); }); + it.each([ + ["(valueof {}) | {}", "(valueof {}) | {} | (valueof []) | []"], + ["(valueof {}) | {}", "(valueof {}) | {}"], + ])(`%s => %s`, async (source, target) => { + await expectTypeAssignable({ source, target }); + }); }); }); }); diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts index a63946a373..f7c4b29c7a 100644 --- a/packages/tspd/src/ref-doc/emitters/markdown.ts +++ b/packages/tspd/src/ref-doc/emitters/markdown.ts @@ -1,4 +1,4 @@ -import { Type, ValueType, getTypeName, resolvePath } from "@typespec/compiler"; +import { Entity, getEntityName, resolvePath } from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -187,8 +187,11 @@ export class MarkdownRenderer { return [base]; } - ref(type: Type | ValueType): string { - const namedType = type.kind !== "Value" && this.refDoc.getNamedTypeRefDoc(type); + ref(type: Entity): string { + const namedType = + type.kind !== "Value" && + type.kind !== "ParamConstraintUnion" && + this.refDoc.getNamedTypeRefDoc(type); if (namedType) { return link( inlinecode(namedType.name), @@ -201,7 +204,7 @@ export class MarkdownRenderer { return inlinecode("{...}"); } return inlinecode( - getTypeName(type, { + getEntityName(type, { namespaceFilter: (ns) => !this.refDoc.namespaces.some((x) => x.name === ns.name), }) ); diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 0c93044f04..6cab6b6e66 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -4,6 +4,7 @@ import { EnumMember, FunctionParameter, FunctionType, + getEntityName, getTypeName, Interface, Model, @@ -124,7 +125,7 @@ function getOperationSignature(type: Operation) { function getFunctionParameterSignature(parameter: FunctionParameter) { const rest = parameter.rest ? "..." : ""; const optional = parameter.optional ? "?" : ""; - return `${rest}${parameter.name}${optional}: ${getTypeName(parameter.type)}`; + return `${rest}${parameter.name}${optional}: ${getEntityName(parameter.type)}`; } function getStringTemplateSignature(stringTemplate: StringTemplate) { From fd019aa228cc1d5be177a201c04d7a4156980818 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 14:20:50 -0700 Subject: [PATCH 032/184] . --- packages/compiler/src/core/checker.ts | 40 +++++++++++++++---- .../compiler/test/checker/decorators.test.ts | 5 --- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 88ed2d0af0..9aa92d4d0d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -33,6 +33,7 @@ import { getFullyQualifiedSymbolName, getParentTemplateNode, isNeverType, + isNullType, isTemplateInstance, isUnknownType, isValueOnly, @@ -3763,7 +3764,9 @@ export function createChecker(program: Program): Checker { ); return errorType; } - const [related, diagnostics] = isTypeAssignableTo(defaultType, type, defaultNode); + const [related, diagnostics] = isValueType(defaultType) + ? isValueOfType(defaultType, type, defaultNode) + : isTypeAssignableTo(defaultType, type, defaultNode); if (!related) { reportCheckerDiagnostics(diagnostics); return errorType; @@ -5598,8 +5601,8 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. */ function isTypeAssignableTo( - source: Type | ParamConstraintUnion | ValueType, - target: Type | ParamConstraintUnion | ValueType, + source: Entity, + target: Entity, diagnosticTarget: DiagnosticTarget ): [boolean, readonly Diagnostic[]] { const [related, diagnostics] = isTypeAssignableToInternal( @@ -5611,6 +5614,26 @@ export function createChecker(program: Program): Checker { return [related === Related.true, diagnostics]; } + /** + * Check if the given Value type is of the given type. + * @param source Value + * @param target Target type + * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. + */ + function isValueOfType( + source: Value, + target: Type, + diagnosticTarget: DiagnosticTarget + ): [boolean, readonly Diagnostic[]] { + const [related, diagnostics] = isValueOfTypeInternal( + source, + target, + diagnosticTarget, + new MultiKeyMap<[Entity, Entity], Related>() + ); + return [related === Related.true, diagnostics]; + } + function isTypeAssignableToInternal( source: Type | ParamConstraintUnion | ValueType, target: Type | ParamConstraintUnion | ValueType, @@ -5776,7 +5799,7 @@ export function createChecker(program: Program): Checker { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - return isValueOfType(source, target.target, diagnosticTarget, relationCache); + return isValueOfTypeInternal(source, target.target, diagnosticTarget, relationCache); } function isAssignableToParameterConstraintUnion( @@ -5810,13 +5833,16 @@ export function createChecker(program: Program): Checker { } /** Check if the value is assignable to the given type. */ - function isValueOfType( + function isValueOfTypeInternal( source: Value, target: Type, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { if (isUnknownType(target)) return [Related.true, []]; + if (isNullType(source)) { + return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); + } switch (source.kind) { case "ObjectLiteral": @@ -5998,7 +6024,7 @@ export function createChecker(program: Program): Checker { } } else { remainingProperties.delete(prop.name); - const [related, propDiagnostics] = isValueOfType( + const [related, propDiagnostics] = isValueOfTypeInternal( sourceProperty, prop.type, diagnosticTarget, @@ -6030,7 +6056,7 @@ export function createChecker(program: Program): Checker { ): [Related, readonly Diagnostic[]] { relationCache.set([source, target], Related.maybe); for (const value of source.values) { - const [related, diagnostics] = isValueOfType( + const [related, diagnostics] = isValueOfTypeInternal( value, target.indexer.value, diagnosticTarget, diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 6cae995993..541715f863 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -353,11 +353,6 @@ describe("compiler: checker: decorators", () => { ); deepStrictEqual(arg, { name: { other: "foo" } }); }); - - it("`: {...}` keeps the ObjectLiteral type", async () => { - const arg = await testCallDecorator("{name: string}", `#{name: "foo"}`); - strictEqual(arg.kind, "ObjectLiteral"); - }); }); describe("passing an tuple literal", () => { From df8dcafd8f066c916aa9b73147dd7a603b0b9917 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 14:25:59 -0700 Subject: [PATCH 033/184] Fix --- packages/compiler/src/core/checker.ts | 1 + packages/compiler/src/core/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 9aa92d4d0d..c1548a8580 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5858,6 +5858,7 @@ export function createChecker(program: Program): Checker { } break; case "String": + case "StringTemplate": case "Number": case "Boolean": case "EnumMember": diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index e4a662f7f2..5db8f3c56d 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -120,6 +120,7 @@ export type Type = export type ValueOnly = ObjectLiteral | TupleLiteral; export type Value = + | StringTemplate | StringLiteral | NumericLiteral | BooleanLiteral From b402ce7df163c7f525d47f002cf14030fd0ddbcd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 15:22:09 -0700 Subject: [PATCH 034/184] Split --- packages/compiler/src/core/checker.ts | 151 ++++++++++-------- .../src/core/helpers/type-name-utils.ts | 8 +- packages/compiler/src/core/js-marshaller.ts | 3 +- packages/compiler/src/core/program.ts | 3 +- packages/compiler/src/core/projector.ts | 17 +- packages/compiler/src/core/semantic-walker.ts | 2 - packages/compiler/src/core/type-utils.ts | 8 +- packages/compiler/src/core/types.ts | 74 +++++---- .../compiler/src/server/type-signature.ts | 4 - packages/compiler/test/checker/values.test.ts | 10 +- .../projection/projector-identity.test.ts | 24 ++- .../json-schema/src/json-schema-emitter.ts | 3 +- packages/openapi3/src/schema-emitter.ts | 3 +- packages/protobuf/src/transform/index.ts | 7 +- .../tspd/src/ref-doc/emitters/markdown.ts | 3 +- .../tspd/src/ref-doc/utils/type-signature.ts | 4 - packages/versioning/src/validate.ts | 5 +- 17 files changed, 197 insertions(+), 132 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c1548a8580..b0a28a45f2 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -32,6 +32,7 @@ import { createProjectionMembers } from "./projection-members.js"; import { getFullyQualifiedSymbolName, getParentTemplateNode, + isErrorType, isNeverType, isNullType, isTemplateInstance, @@ -265,7 +266,7 @@ export interface Checker { resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]]; /** @internal */ - getTypeOrValueForNode(node: Node): Type; + getTypeOrValueForNode(node: Node): Type | Value; errorType: ErrorType; voidType: VoidType; @@ -655,6 +656,15 @@ export function createChecker(program: Program): Checker { } function getTypeForNode(node: Node, mapper?: TypeMapper): Type { + const type = getTypeOrValueForNode(node, mapper); + if (isValueOnly(type)) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return errorType; + } + return type; + } + + function getTypeOrValueForNode(node: Node, mapper?: TypeMapper): Type | Value { switch (node.kind) { case SyntaxKind.ModelExpression: return checkModel(node, mapper); @@ -716,8 +726,9 @@ export function createChecker(program: Program): Checker { case SyntaxKind.UnknownKeyword: return unknownType; case SyntaxKind.ObjectLiteral: + return checkObjectLiteral(node, mapper); case SyntaxKind.TupleLiteral: - reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return checkTupleLiteral(node, mapper); return errorType; } @@ -770,10 +781,22 @@ export function createChecker(program: Program): Checker { return Boolean(type.namespace && isTypeSpecNamespace(type.namespace)); } + function checkTemplateParameterDeclaration( + node: TemplateParameterDeclarationNode, + mapper: undefined + ): TemplateParameter; + function checkTemplateParameterDeclaration( + node: TemplateParameterDeclarationNode, + mapper: TypeMapper + ): Type | Value; function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: TypeMapper | undefined - ): Type { + ): Type | Value; + function checkTemplateParameterDeclaration( + node: TemplateParameterDeclarationNode, + mapper: TypeMapper | undefined + ): Type | Value { const parentNode = node.parent!; const grandParentNode = parentNode.parent; const links = getSymbolLinks(node.symbol); @@ -847,7 +870,7 @@ export function createChecker(program: Program): Checker { nodeDefault: Expression, templateParameters: readonly TemplateParameterDeclarationNode[], index: number, - constraint: Type | ParamConstraintUnion | ValueType | undefined + constraint: Entity | undefined ) { function visit(node: Node) { const type = getTypeOrValueForNode(node); @@ -902,7 +925,10 @@ export function createChecker(program: Program): Checker { return type; } - function checkTemplateArgument(node: TemplateArgumentNode, mapper: TypeMapper | undefined): Type { + function checkTemplateArgument( + node: TemplateArgumentNode, + mapper: TypeMapper | undefined + ): Type | Value { return getTypeOrValueForNode(node.argument, mapper); } @@ -986,13 +1012,13 @@ export function createChecker(program: Program): Checker { args: readonly TemplateArgumentNode[], decls: readonly TemplateParameterDeclarationNode[], mapper: TypeMapper | undefined - ): Map { + ): Map { const params = new Map(); const positional: TemplateParameter[] = []; interface TemplateParameterInit { decl: TemplateParameterDeclarationNode; // Deferred initializer so that we evaluate the param arguments in definition order. - checkArgument: (() => [Node, Type]) | null; + checkArgument: (() => [Node, Type | Value]) | null; } const initMap = new Map( decls.map(function (decl) { @@ -1014,7 +1040,7 @@ export function createChecker(program: Program): Checker { let named = false; for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { - function deferredCheck(): [Node, Type] { + function deferredCheck(): [Node, Type | Value] { return [arg, getTypeOrValueForNode(arg.argument, mapper)]; } @@ -1081,11 +1107,11 @@ export function createChecker(program: Program): Checker { } } - const finalMap = initMap as unknown as Map; + const finalMap = initMap as unknown as Map; const mapperParams: TemplateParameter[] = []; - const mapperArgs: Type[] = []; + const mapperArgs: (Type | Value)[] = []; for (const [param, { decl, checkArgument: init }] of [...initMap]) { - function commit(param: TemplateParameter, type: Type): void { + function commit(param: TemplateParameter, type: Type | Value): void { finalMap.set(param, type); mapperParams.push(param); mapperArgs.push(type); @@ -1249,10 +1275,11 @@ export function createChecker(program: Program): Checker { compilerAssert(sym.type, "Expected late bound symbol to have type"); return sym.type; } else if (sym.flags & SymbolFlags.TemplateParameter) { - baseType = checkTemplateParameterDeclaration( + const mapped = checkTemplateParameterDeclaration( sym.declarations[0] as TemplateParameterDeclarationNode, mapper ); + baseType = mapped as any; } else if (symbolLinks.type) { // Have a cached type for non-declarations baseType = symbolLinks.type; @@ -1323,23 +1350,29 @@ export function createChecker(program: Program): Checker { node: TemplateableNode, mapper: TypeMapper | undefined ): Type { - return sym.flags & SymbolFlags.Model - ? checkModelStatement(node as ModelStatementNode, mapper) - : sym.flags & SymbolFlags.Scalar - ? checkScalar(node as ScalarStatementNode, mapper) - : sym.flags & SymbolFlags.Alias - ? checkAlias(node as AliasStatementNode, mapper) - : sym.flags & SymbolFlags.Interface - ? checkInterface(node as InterfaceStatementNode, mapper) - : sym.flags & SymbolFlags.Operation - ? checkOperation(node as OperationStatementNode, mapper) - : checkUnion(node as UnionStatementNode, mapper); + const type = + sym.flags & SymbolFlags.Model + ? checkModelStatement(node as ModelStatementNode, mapper) + : sym.flags & SymbolFlags.Scalar + ? checkScalar(node as ScalarStatementNode, mapper) + : sym.flags & SymbolFlags.Alias + ? checkAlias(node as AliasStatementNode, mapper) + : sym.flags & SymbolFlags.Interface + ? checkInterface(node as InterfaceStatementNode, mapper) + : sym.flags & SymbolFlags.Operation + ? checkOperation(node as OperationStatementNode, mapper) + : checkUnion(node as UnionStatementNode, mapper); + if (isValueOnly(type)) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return errorType; + } + return type; } function getOrInstantiateTemplate( templateNode: TemplateableNode, params: TemplateParameter[], - args: Type[], + args: (Type | Value)[], parentMapper: TypeMapper | undefined, instantiateTempalates = true ): Type { @@ -1609,17 +1642,6 @@ export function createChecker(program: Program): Checker { return parameterType; } - function getTypeOrValueForNode(node: Node, mapper?: TypeMapper): Type | Value { - switch (node.kind) { - case SyntaxKind.ObjectLiteral: - return checkObjectLiteral(node, mapper); - case SyntaxKind.TupleLiteral: - return checkTupleLiteral(node, mapper); - default: - return getTypeForNode(node, mapper); - } - } - function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): Type | ValueType { switch (node.kind) { case SyntaxKind.ValueOfExpression: @@ -3026,11 +3048,11 @@ export function createChecker(program: Program): Checker { node: ObjectLiteralNode, mapper: TypeMapper | undefined ): ObjectLiteral { - return createAndFinishType({ + return { kind: "ObjectLiteral", node: node, properties: checkObjectLiteralProperties(node, mapper), - }); + }; } function checkObjectLiteralProperties( @@ -3057,7 +3079,7 @@ export function createChecker(program: Program): Checker { return properties; } - function checkIsValue(type: Type, diagnosticTarget: DiagnosticTarget): type is Value { + function checkIsValue(type: Type | Value, diagnosticTarget: DiagnosticTarget): type is Value { if (!isValueType(type)) { reportCheckerDiagnostic( createDiagnostic({ @@ -3099,11 +3121,11 @@ export function createChecker(program: Program): Checker { } }) .filter(isDefined); - return createAndFinishType({ + return { kind: "TupleLiteral", node: node, values, - }); + }; } function createUnion(options: Type[]): Union { @@ -3728,7 +3750,7 @@ export function createChecker(program: Program): Checker { }; } - function isDefaultValue(type: Type): boolean { + function isDefaultValue(type: Type | Value): boolean { if (type.kind === "UnionVariant") { return isValueType(type.type); } @@ -3749,7 +3771,7 @@ export function createChecker(program: Program): Checker { return isValueType(type); } - function checkDefault(defaultNode: Node, type: Type): Type { + function checkDefault(defaultNode: Node, type: Type): Type | Value { const defaultType = getTypeOrValueForNode(defaultNode, undefined); if (isErrorType(type)) { return errorType; @@ -3933,7 +3955,7 @@ export function createChecker(program: Program): Checker { return type.kind === "Model" ? type.indexer?.value : undefined; } - function resolveDecoratorArgJsValue(value: Type, valueOf: boolean) { + function resolveDecoratorArgJsValue(value: Type | Value, valueOf: boolean) { if (valueOf) { return marshallTypeForJS(value); } @@ -3941,8 +3963,8 @@ export function createChecker(program: Program): Checker { } function checkArgumentAssignable( - argumentType: Type, - parameterType: Type | ParamConstraintUnion | ValueType, + argumentType: Type | Value, + parameterType: Entity, diagnosticTarget: DiagnosticTarget ): boolean { const [valid] = isTypeAssignableTo(argumentType, parameterType, diagnosticTarget); @@ -3951,7 +3973,7 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "invalid-argument", format: { - value: getTypeName(argumentType), + value: getEntityName(argumentType), expected: getEntityName(parameterType), }, target: diagnosticTarget, @@ -4110,7 +4132,7 @@ export function createChecker(program: Program): Checker { return extendsType; } - function checkAlias(node: AliasStatementNode, mapper: TypeMapper | undefined): Type { + function checkAlias(node: AliasStatementNode, mapper: TypeMapper | undefined): Type | Value { const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { @@ -4135,7 +4157,9 @@ export function createChecker(program: Program): Checker { pendingResolutions.start(aliasSymId, ResolutionKind.Type); const type = getTypeOrValueForNode(node.value, mapper); - linkType(links, type, mapper); + if (!isValueOnly(type)) { + linkType(links, type, mapper); + } pendingResolutions.finish(aliasSymId, ResolutionKind.Type); return type; @@ -5583,8 +5607,8 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. */ function checkTypeAssignable( - source: Type | ParamConstraintUnion | ValueType, - target: Type | ParamConstraintUnion | ValueType, + source: Entity, + target: Entity, diagnosticTarget: DiagnosticTarget ): boolean { const [related, diagnostics] = isTypeAssignableTo(source, target, diagnosticTarget); @@ -5635,8 +5659,8 @@ export function createChecker(program: Program): Checker { } function isTypeAssignableToInternal( - source: Type | ParamConstraintUnion | ValueType, - target: Type | ParamConstraintUnion | ValueType, + source: Entity, + target: Entity, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { @@ -5689,6 +5713,9 @@ export function createChecker(program: Program): Checker { if (target.kind === "Value") { return isAssignableToValueType(source, target, diagnosticTarget, relationCache); } + if (isValueOnly(target)) { + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + } if (target.kind === "ParamConstraintUnion") { return isAssignableToParameterConstraintUnion( source, @@ -5782,7 +5809,7 @@ export function createChecker(program: Program): Checker { } function isAssignableToValueType( - source: Type | ParamConstraintUnion | ValueType, + source: Entity, target: ValueType, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> @@ -5803,7 +5830,7 @@ export function createChecker(program: Program): Checker { } function isAssignableToParameterConstraintUnion( - source: Type | ParamConstraintUnion | ValueType, + source: Entity, target: ParamConstraintUnion, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> @@ -6016,8 +6043,8 @@ export function createChecker(program: Program): Checker { code: "missing-property", format: { propertyName: prop.name, - sourceType: getTypeName(source), - targetType: getTypeName(target), + sourceType: getEntityName(source), + targetType: getEntityName(target), }, target: source, }) @@ -6079,7 +6106,7 @@ export function createChecker(program: Program): Checker { } function arePropertiesAssignableToIndexer( - properties: Map, + properties: Map, indexerConstaint: Type, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> @@ -6183,7 +6210,7 @@ export function createChecker(program: Program): Checker { code: "unassignable", messageId: "withDetails", format: { - sourceType: getTypeName(source), + sourceType: getEntityName(source), targetType: getTypeName(target), details: `Source has ${source.values.length} element(s) but target requires ${target.values.length}.`, }, @@ -6289,10 +6316,6 @@ function isAnonymous(type: Type) { return !("name" in type) || typeof type.name !== "string" || !type.name; } -function isErrorType(type: Type): type is ErrorType { - return type.kind === "Intrinsic" && type.name === "ErrorType"; -} - const numericRanges: Record< string, [min: number | bigint, max: number | bigint, options: { int: boolean }] @@ -6349,10 +6372,10 @@ function addDerivedModels(models: Set, possiblyDerivedModels: ReadonlySet function createTypeMapper( parameters: TemplateParameter[], - args: Type[], + args: (Type | Value)[], parentMapper?: TypeMapper ): TypeMapper { - const map = new Map(parentMapper?.map ?? []); + const map = new Map(parentMapper?.map ?? []); for (const [index, param] of parameters.entries()) { map.set(param, args[index]); diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 7b07c67c18..b1f411a2f0 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -63,9 +63,9 @@ export function getEntityName(type: Entity, options?: TypeNameOptions): string { case "ParamConstraintUnion": return type.options.map((x) => getEntityName(x, options)).join(" | "); case "ObjectLiteral": - return `#{${[...type.properties.entries()].map(([name, value]) => `${name}: ${getTypeName(value, options)}`).join(", ")}}`; + return `#{${[...type.properties.entries()].map(([name, value]) => `${name}: ${getEntityName(value, options)}`).join(", ")}}`; case "TupleLiteral": - return `#[${type.values.map((x) => getTypeName(x, options)).join(", ")}]`; + return `#[${type.values.map((x) => getEntityName(x, options)).join(", ")}]`; } return `(unnamed type)`; @@ -137,7 +137,7 @@ function getModelName(model: Model, options: TypeNameOptions | undefined) { const modelName = nsPrefix + getIdentifierName(model.name, options); if (isTemplateInstance(model)) { // template instantiation - const args = model.templateMapper.args.map((x) => getTypeName(x, options)); + const args = model.templateMapper.args.map((x) => getEntityName(x, options)); return `${modelName}<${args.join(", ")}>`; } else if ((model.node as ModelStatementNode)?.templateParameters?.length > 0) { // template @@ -185,7 +185,7 @@ function getInterfaceName(iface: Interface, options: TypeNameOptions | undefined let interfaceName = getIdentifierName(iface.name, options); if (isTemplateInstance(iface)) { interfaceName += `<${iface.templateMapper.args - .map((x) => getTypeName(x, options)) + .map((x) => getEntityName(x, options)) .join(", ")}>`; } return `${getNamespacePrefix(iface.namespace, options)}${interfaceName}`; diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index b0e379351c..547a56989b 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -7,9 +7,10 @@ import type { StringLiteral, TupleLiteral, Type, + Value, } from "./types.js"; -export function marshallTypeForJS(type: T): MarshalledValue { +export function marshallTypeForJS(type: T): MarshalledValue { switch (type.kind) { case "Boolean": case "String": diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index bf6f711bc5..740e3db9d5 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -45,6 +45,7 @@ import { DirectiveExpressionNode, EmitContext, EmitterFunc, + Entity, JsSourceFileNode, LibraryInstance, LibraryMetadata, @@ -1162,7 +1163,7 @@ export async function compile( } } - function getNode(target: Node | Type | Sym): Node | undefined { + function getNode(target: Node | Entity | Sym): Node | undefined { if (!("kind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index 21a455d6eb..70a007adaa 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -2,7 +2,12 @@ import { createRekeyableMap, mutate } from "../utils/misc.js"; import { finishTypeForProgram } from "./checker.js"; import { compilerAssert } from "./diagnostics.js"; import { Program, ProjectedProgram, createStateAccessors, isProjectedProgram } from "./program.js"; -import { getParentTemplateNode, isNeverType, isTemplateInstance } from "./type-utils.js"; +import { + getParentTemplateNode, + isNeverType, + isTemplateInstance, + isValueOnly, +} from "./type-utils.js"; import { DecoratorApplication, DecoratorArgument, @@ -21,6 +26,8 @@ import { TypeMapper, Union, UnionVariant, + Value, + ValueOnly, } from "./types.js"; /** @@ -94,7 +101,13 @@ export function createProjector( return projectedProgram; - function projectType(type: Type): Type { + function projectType(type: Type): Type; + function projectType(type: ValueOnly): ValueOnly; + function projectType(type: Type | Value): Type | Value; + function projectType(type: Type | Value): Type | Value { + if (isValueOnly(type)) { + return type; + } if (projectedTypes.has(type)) { return projectedTypes.get(type)!; } diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 6a0e4f2f16..f704d1702f 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -384,8 +384,6 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { case "Decorator": return navigateDecoratorDeclaration(type, context); case "Object": - case "ObjectLiteral": - case "TupleLiteral": case "Projection": case "Function": case "FunctionParameter": diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index c25f914e7c..d16ed3ada1 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -24,15 +24,15 @@ import { VoidType, } from "./types.js"; -export function isErrorType(type: Type): type is ErrorType { +export function isErrorType(type: Entity): type is ErrorType { return type.kind === "Intrinsic" && type.name === "ErrorType"; } -export function isVoidType(type: Type): type is VoidType { +export function isVoidType(type: Entity): type is VoidType { return type.kind === "Intrinsic" && type.name === "void"; } -export function isNeverType(type: Type): type is NeverType { +export function isNeverType(type: Entity): type is NeverType { return type.kind === "Intrinsic" && type.name === "never"; } @@ -65,7 +65,7 @@ export function isValueType(type: Entity): type is Value { return valueTypes.has(type.kind); } -export function isValueOnly(type: Type): type is ValueOnly { +export function isValueOnly(type: Entity): type is ValueOnly { switch (type.kind) { case "ObjectLiteral": case "TupleLiteral": diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 5db8f3c56d..d3f1c6ffb3 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -22,11 +22,11 @@ export type MarshalledValue = export type DecoratorArgumentValue = Type | number | string | boolean; export interface DecoratorArgument { - value: Type; + value: Type | Value; /** * Marshalled value for use in Javascript. */ - jsValue: Type | Record | unknown[] | string | number | boolean; + jsValue: Type | Value | Record | unknown[] | string | number | boolean; node?: Node; } @@ -74,9 +74,9 @@ export type TemplatedType = Model | Operation | Interface | Union | Scalar; export interface TypeMapper { partial: boolean; - getMappedType(type: TemplateParameter): Type; - args: readonly Type[]; - /** @internal */ map: Map; + getMappedType(type: TemplateParameter): Type | Value; + args: readonly (Type | Value)[]; + /** @internal */ map: Map; } export interface TemplatedTypeBase { @@ -84,26 +84,41 @@ export interface TemplatedTypeBase { /** * @deprecated use templateMapper instead. */ - templateArguments?: Type[]; + templateArguments?: (Type | Value)[]; templateNode?: Node; } +/** + * Represent every single entity that are part of the TypeSpec program. Those are composed of different elements: + * - Types + * - Values + * - Value Constraints + */ export type Entity = Type | Value | ValueType | ParamConstraintUnion; -export type Type = +/** Entities that can be used as both values and values. */ +export type TypeAndValue = + | StringLiteral + | StringTemplate + | NumericLiteral + | BooleanLiteral + | EnumMember; + +/** + * Entities that can only be used as value. + */ +export type ValueOnly = ObjectLiteral | TupleLiteral; + +/** Entities that can be used as types only */ +export type TypeOnly = | Model | ModelProperty | Scalar | Interface | Enum - | EnumMember | TemplateParameter | Namespace | Operation - | StringLiteral - | NumericLiteral - | BooleanLiteral - | StringTemplate | StringTemplateSpan | Tuple | Union @@ -113,20 +128,15 @@ export type Type = | Decorator | FunctionParameter | ObjectType - | ObjectLiteral - | TupleLiteral | Projection; -export type ValueOnly = ObjectLiteral | TupleLiteral; +/** Entities that can be used as types */ +export type Type = TypeAndValue | TypeOnly; -export type Value = - | StringTemplate - | StringLiteral - | NumericLiteral - | BooleanLiteral - | EnumMember - | ObjectLiteral - | TupleLiteral; +/** + * Entities that can be used as values. + */ +export type Value = TypeAndValue | ValueOnly; export type StdTypes = { // Models @@ -160,7 +170,7 @@ export interface Projector { parentProjector?: Projector; projections: ProjectionApplication[]; projectedTypes: Map; - projectType(type: Type): Type; + projectType(type: Type | Value): Type | Value; projectedStartNode?: Type; projectedGlobalNamespace?: Namespace; } @@ -302,17 +312,19 @@ export interface ModelProperty extends BaseType, DecoratedType { // this tracks the property we copied from. sourceProperty?: ModelProperty; optional: boolean; - default?: Type; + default?: Type | Value; model?: Model; } -export interface ObjectLiteral extends BaseType { +export interface ObjectLiteral { kind: "ObjectLiteral"; + node: ObjectLiteralNode; properties: Map; } -export interface TupleLiteral extends BaseType { +export interface TupleLiteral { kind: "TupleLiteral"; + node: TupleLiteralNode; values: Value[]; } @@ -581,7 +593,7 @@ export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; constraint?: Type | ParamConstraintUnion | ValueType; - default?: Type; + default?: Type | Value; } export interface Decorator extends BaseType { @@ -735,8 +747,8 @@ export const enum SymbolFlags { * Maps type arguments to instantiated type. */ export interface TypeInstantiationMap { - get(args: readonly Type[]): Type | undefined; - set(args: readonly Type[], type: Type): void; + get(args: readonly (Type | Value)[]): Type | undefined; + set(args: readonly (Type | Value)[], type: Type): void; } /** @@ -1906,7 +1918,7 @@ export interface SourceLocation extends TextRange { export const NoTarget = Symbol.for("NoTarget"); /** Diagnostic target that can be used when working with TypeSpec types. */ -export type TypeSpecDiagnosticTarget = Node | Type | Sym; +export type TypeSpecDiagnosticTarget = Node | Entity | Sym; export type DiagnosticTarget = TypeSpecDiagnosticTarget | SourceLocation; export type DiagnosticSeverity = "error" | "warning"; diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index a8d8192a45..7ee0480702 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -77,10 +77,6 @@ function getTypeSignature(type: Type | ValueType): string { return "(projection)"; case "Object": return "(object)"; - case "ObjectLiteral": - return fence("#{...}"); - case "TupleLiteral": - return fence("#[...]"); default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); diff --git a/packages/compiler/test/checker/values.test.ts b/packages/compiler/test/checker/values.test.ts index 557558b2a2..8067f6b3bf 100644 --- a/packages/compiler/test/checker/values.test.ts +++ b/packages/compiler/test/checker/values.test.ts @@ -1,6 +1,6 @@ import { ok, strictEqual } from "assert"; import { describe, it } from "vitest"; -import { Diagnostic, Type } from "../../src/index.js"; +import { Diagnostic, Type, Value } from "../../src/index.js"; import { createTestHost, createTestRunner, @@ -21,11 +21,11 @@ async function diagnoseUsage( async function compileAndDiagnoseValueType( code: string, other?: string -): Promise<[Type | undefined, readonly Diagnostic[]]> { +): Promise<[Value | undefined, readonly Diagnostic[]]> { const host = await createTestHost(); - let called: Type | undefined; + let called: Value | undefined; host.addJsFile("dec.js", { - $collect: (context: DecoratorContext, target: Type, value: Type) => { + $collect: (context: DecoratorContext, target: Type, value: Value) => { called = value; }, }); @@ -44,7 +44,7 @@ async function compileAndDiagnoseValueType( return [called, diagnostics]; } -async function compileValueType(code: string, other?: string): Promise { +async function compileValueType(code: string, other?: string): Promise { const [called, diagnostics] = await compileAndDiagnoseValueType(code, other); expectDiagnosticEmpty(diagnostics); ok(called, "Decorator was not called"); diff --git a/packages/compiler/test/projection/projector-identity.test.ts b/packages/compiler/test/projection/projector-identity.test.ts index 7cb086dcdb..05962b5cc7 100644 --- a/packages/compiler/test/projection/projector-identity.test.ts +++ b/packages/compiler/test/projection/projector-identity.test.ts @@ -1,6 +1,12 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { DecoratorContext, Namespace, Type, getTypeName } from "../../src/core/index.js"; +import { + DecoratorContext, + Namespace, + Type, + getTypeName, + isValueOnly, +} from "../../src/core/index.js"; import { createProjector } from "../../src/core/projector.js"; import { createTestHost, createTestRunner } from "../../src/testing/test-host.js"; import { BasicTestRunner, TestHost } from "../../src/testing/types.js"; @@ -376,16 +382,24 @@ describe("compiler: projector: Identity", () => { ok(value !== original.templateMapper.map.get(key)); } for (const arg of original.templateMapper.args) { - ok(arg.projector === original.projector); + if (!isValueOnly(arg)) { + ok(arg.projector === original.projector); + } } for (const value of original.templateMapper.map.values()) { - ok(value.projector === original.projector); + if (!isValueOnly(value)) { + ok(value.projector === original.projector); + } } for (const arg of projected.templateMapper.args) { - ok(arg.projector === projected.projector); + if (!isValueOnly(arg)) { + ok(arg.projector === projected.projector); + } } for (const value of projected.templateMapper.map.values()) { - ok(value.projector === projected.projector); + if (!isValueOnly(value)) { + ok(value.projector === projected.projector); + } } } }); diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index eaec06d930..fa86e51115 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -37,6 +37,7 @@ import { typespecTypeToJson, Union, UnionVariant, + Value, } from "@typespec/compiler"; import { ArrayBuilder, @@ -181,7 +182,7 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche return result; } - #getDefaultValue(type: Type, defaultType: Type): any { + #getDefaultValue(type: Type, defaultType: Type | Value): any { const program = this.emitter.getProgram(); switch (defaultType.kind) { diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 0adc0e62c3..87e66a4a43 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -17,6 +17,7 @@ import { TypeNameOptions, Union, UnionVariant, + Value, compilerAssert, getDeprecated, getDiscriminatedUnion, @@ -949,7 +950,7 @@ const B = { }, } as const; -export function getDefaultValue(program: Program, type: Type, defaultType: Type): any { +export function getDefaultValue(program: Program, type: Type, defaultType: Type | Value): any { switch (defaultType.kind) { case "String": return defaultType.value; diff --git a/packages/protobuf/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index 1f620e1779..d5ad05278f 100644 --- a/packages/protobuf/src/transform/index.ts +++ b/packages/protobuf/src/transform/index.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { + compilerAssert, DiagnosticTarget, Enum, formatDiagnostic, @@ -13,6 +14,7 @@ import { IntrinsicType, isDeclaredInNamespace, isTemplateInstance, + isValueOnly, Model, ModelProperty, Namespace, @@ -536,8 +538,10 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P function mapToProto(t: Model, relativeSource: Model | Operation): ProtoMap { const [keyType, valueType] = t.templateMapper!.args; + compilerAssert(!isValueOnly(keyType), "Cannot be a value type"); + compilerAssert(!isValueOnly(valueType), "Cannot be a value type"); // A map's value cannot be another map. - if (isMap(program, valueType)) { + if (isMap(program, keyType)) { reportDiagnostic(program, { code: "unsupported-field-type", messageId: "recursive-map", @@ -558,6 +562,7 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P function arrayToProto(t: Model, relativeSource: Model | Operation): ProtoType { const valueType = (t as Model).templateMapper!.args[0]; + compilerAssert(!isValueOnly(valueType), "Cannot be a value type"); // Nested arrays are not supported. if (isArray(valueType)) { diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts index f7c4b29c7a..b8c3a201c2 100644 --- a/packages/tspd/src/ref-doc/emitters/markdown.ts +++ b/packages/tspd/src/ref-doc/emitters/markdown.ts @@ -1,4 +1,4 @@ -import { Entity, getEntityName, resolvePath } from "@typespec/compiler"; +import { Entity, getEntityName, isValueOnly, resolvePath } from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -191,6 +191,7 @@ export class MarkdownRenderer { const namedType = type.kind !== "Value" && type.kind !== "ParamConstraintUnion" && + !isValueOnly(type) && this.refDoc.getNamedTypeRefDoc(type); if (namedType) { return link( diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 6cab6b6e66..04095a9f85 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -68,10 +68,6 @@ export function getTypeSignature(type: Type | ValueType): string { return "(projection)"; case "Object": return "(object)"; - case "ObjectLiteral": - return "#{...}"; - case "TupleLiteral": - return "#[...]"; default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); diff --git a/packages/versioning/src/validate.ts b/packages/versioning/src/validate.ts index d3dd6f4e59..ada20803d1 100644 --- a/packages/versioning/src/validate.ts +++ b/packages/versioning/src/validate.ts @@ -3,6 +3,7 @@ import { getService, getTypeName, isTemplateInstance, + isValueOnly, Namespace, navigateProgram, NoTarget, @@ -473,7 +474,9 @@ function validateReference(program: Program, source: Type, target: Type) { if ("templateMapper" in target) { for (const param of target.templateMapper?.args ?? []) { - validateTargetVersionCompatible(program, source, param); + if (!isValueOnly(param)) { + validateTargetVersionCompatible(program, source, param); + } } } From 34adfbe2abcaf2ad04e28856340ecf8135607faf Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 15:31:23 -0700 Subject: [PATCH 035/184] Create feature-object-literals-2024-2-18-22-23-26.md --- .../feature-object-literals-2024-2-18-22-23-26.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .chronus/changes/feature-object-literals-2024-2-18-22-23-26.md diff --git a/.chronus/changes/feature-object-literals-2024-2-18-22-23-26.md b/.chronus/changes/feature-object-literals-2024-2-18-22-23-26.md new file mode 100644 index 0000000000..966b10a2b4 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-2-18-22-23-26.md @@ -0,0 +1,10 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/json-schema" + - "@typespec/protobuf" + - "@typespec/versioning" +--- + +Update to support new value types From 14efb01ef8873062bffae03ef95c1d4256c8c745 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 15:46:44 -0700 Subject: [PATCH 036/184] fix --- packages/compiler/src/core/checker.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index b0a28a45f2..b7ad1f3e3a 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1349,7 +1349,7 @@ export function createChecker(program: Program): Checker { sym: Sym, node: TemplateableNode, mapper: TypeMapper | undefined - ): Type { + ): Type | Value { const type = sym.flags & SymbolFlags.Model ? checkModelStatement(node as ModelStatementNode, mapper) @@ -1362,10 +1362,7 @@ export function createChecker(program: Program): Checker { : sym.flags & SymbolFlags.Operation ? checkOperation(node as OperationStatementNode, mapper) : checkUnion(node as UnionStatementNode, mapper); - if (isValueOnly(type)) { - reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); - return errorType; - } + return type; } @@ -2797,7 +2794,7 @@ export function createChecker(program: Program): Checker { function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { - getTypeForNode(statement, undefined); + getTypeOrValueForNode(statement, undefined); } } From d253264c8b4c3e1e107de51c7f36d00e73d22b1f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 19:40:38 -0700 Subject: [PATCH 037/184] Test --- packages/compiler/src/core/checker.ts | 65 ++++++++++++++++--- packages/compiler/src/core/js-marshaller.ts | 29 +++++++-- .../compiler/test/checker/decorators.test.ts | 41 +++++++++++- .../compiler/test/checker/relation.test.ts | 24 ++++++- 4 files changed, 141 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index b7ad1f3e3a..546615074d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -9,6 +9,7 @@ import { } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; +import { createModelToLiteralCodeFix } from "./compiler-code-fixes/model-to-literal.codefix.js"; import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; @@ -19,7 +20,7 @@ import { getNamespaceFullName, getTypeName, } from "./helpers/index.js"; -import { marshallTypeForJS } from "./js-marshaller.js"; +import { marshallTypeForJSWithLegacyCast, tryMarshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { exprIsBareIdentifier, @@ -3936,11 +3937,12 @@ export function createChecker(program: Program): Checker { } const arg = args[index]; if (arg && arg.value) { - resolvedArgs.push({ - ...arg, - jsValue: resolveDecoratorArgJsValue(arg.value, parameter.type.kind === "Value"), - }); - if (!checkArgumentAssignable(arg.value, parameter.type, arg.node!)) { + if (checkArgumentAssignable(arg.value, parameter.type, arg.node!)) { + resolvedArgs.push({ + ...arg, + jsValue: resolveDecoratorArgJsValue(arg.value, parameter.type.kind === "Value"), + }); + } else { hasError = true; } } @@ -3954,7 +3956,13 @@ export function createChecker(program: Program): Checker { function resolveDecoratorArgJsValue(value: Type | Value, valueOf: boolean) { if (valueOf) { - return marshallTypeForJS(value); + if (isValueType(value) || value.kind === "Model" || value.kind === "Tuple") { + const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value); + reportCheckerDiagnostics(diagnostics); + return res ?? value; // TODO: can this be a compilerAssert + } else { + return value; + } } return value; } @@ -5467,7 +5475,7 @@ export function createChecker(program: Program): Checker { if (!ref) throw new ProjectionError("Can't find decorator."); compilerAssert(ref.flags & SymbolFlags.Decorator, "should only resolve decorator symbols"); return createFunctionType((...args: Type[]): Type => { - ref.value!({ program }, ...args.map(marshallTypeForJS)); + ref.value!({ program }, ...args.map(tryMarshallTypeForJS)); return voidType; }); } @@ -5494,7 +5502,7 @@ export function createChecker(program: Program): Checker { } else if (ref.flags & SymbolFlags.Function) { // TODO: store this in a symbol link probably? const t: FunctionType = createFunctionType((...args: Type[]): Type => { - const retval = ref.value!(program, ...args.map(marshallTypeForJS)); + const retval = ref.value!(program, ...args.map(tryMarshallTypeForJS)); return marshalProjectionReturn(retval, { functionName: node.sv }); }); return t; @@ -5819,6 +5827,45 @@ export function createChecker(program: Program): Checker { relationCache ); } + + // LEGACY BEHAVIOR - Goal here is to all models instead of object literal and tuple instead of tuple literals to get a smooth migration of decorators + if ( + source.kind === "Tuple" && + isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === + Related.true + ) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createTupleToLiteralCodeFix(source.node)], + format: { + message: + "Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", + }, + target: source.node, + }) + ); + return [Related.true, []]; + } else if ( + source.kind === "Model" && + source.node?.kind === SyntaxKind.ModelExpression && + isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === + Related.true + ) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createModelToLiteralCodeFix(source.node)], + format: { + message: + "Using a model as a value is deprecated. Use an object literal instead(with #{}).", + }, + target: source, + }) + ); + return [Related.true, []]; + } + if (!isValueType(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 547a56989b..5cc88678bd 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,16 +1,38 @@ -import { stringTemplateToString } from "./index.js"; +import { isValueType, stringTemplateToString, typespecTypeToJson } from "./index.js"; import type { BooleanLiteral, + Diagnostic, MarshalledValue, + Model, NumericLiteral, ObjectLiteral, StringLiteral, + Tuple, TupleLiteral, Type, Value, } from "./types.js"; -export function marshallTypeForJS(type: T): MarshalledValue { +export function tryMarshallTypeForJS(type: T): MarshalledValue { + if (isValueType(type)) { + return marshallTypeForJS(type); + } + return type as any; +} + +/** Legacy version that will cast models to object literals and tuple to tuple literals */ +export function marshallTypeForJSWithLegacyCast( + type: T +): [MarshalledValue | undefined, readonly Diagnostic[]] { + switch (type.kind) { + case "Model": + case "Tuple": + return typespecTypeToJson(type, type) as any; + default: + return [marshallTypeForJS(type) as any, []]; + } +} +export function marshallTypeForJS(type: T): MarshalledValue { switch (type.kind) { case "Boolean": case "String": @@ -22,8 +44,7 @@ export function marshallTypeForJS(type: T): MarshalledVa return objectLiteralToValue(type) as any; case "TupleLiteral": return tupleLiteralToValue(type) as any; - // In other case we keep the original tye - default: + case "EnumMember": return type as any; } } diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 541715f863..1a313dbc3b 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -288,10 +288,15 @@ describe("compiler: checker: decorators", () => { }); describe("value marshalling", () => { - async function testCallDecorator(type: string, value: string): Promise { + async function testCallDecorator( + type: string, + value: string, + suppress?: boolean + ): Promise { await runner.compile(` extern dec testDec(target: unknown, arg1: ${type}); - + + ${suppress ? `#suppress "deprecated" "for testing"` : ""} @testDec(${value}) @test model Foo {} @@ -366,6 +371,38 @@ describe("compiler: checker: decorators", () => { deepStrictEqual(arg, [["foo"]]); }); }); + + // This functionality is to provide a smooth transition from the old way of passing a model/tuple as values + // It is to be removed in the future. + describe("legacy type to value casting", () => { + describe("passing an model gets converted to an object", () => { + it("valueof model cast the tuple to a JS object", async () => { + const arg = await testCallDecorator("valueof {name: string}", `{name: "foo"}`, true); + deepStrictEqual(arg, { name: "foo" }); + }); + + it("valueof model cast the tuple recursively to a JS object", async () => { + const arg = await testCallDecorator( + "valueof {name: unknown}", + `{name: {other: "foo"}}`, + true + ); + deepStrictEqual(arg, { name: { other: "foo" } }); + }); + }); + + describe("passing an tuple gets converted to an object", () => { + it("valueof model cast the tuple to a JS array", async () => { + const arg = await testCallDecorator("valueof string[]", `["foo"]`, true); + deepStrictEqual(arg, ["foo"]); + }); + + it("valueof model cast the tuple recursively to a JS object", async () => { + const arg = await testCallDecorator("valueof unknown[]", `[["foo"]]`, true); + deepStrictEqual(arg, [["foo"]]); + }); + }); + }); }); }); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index a53938c79d..43d257e607 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1270,7 +1270,16 @@ describe("compiler: checker: type relations", () => { }); }); - it("cannot assign a model ", async () => { + it("can assign a model (LEGACY)", async () => { + await expectTypeAssignable({ + source: `{name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }); + }); + + // Disabled for now as this is allowed for backcompat + it.skip("cannot assign a model ", async () => { await expectTypeNotAssignable( { source: `{name: "foo"}`, @@ -1325,7 +1334,15 @@ describe("compiler: checker: type relations", () => { }); }); - it("cannot assign a tuple", async () => { + it("can assign a tuple (LEGACY)", async () => { + await expectValueAssignable({ + source: `["foo"]`, + target: "valueof string[]", + }); + }); + + // Disabled for now as this is allowed for backcompat + it.skip("cannot assign a tuple", async () => { await expectValueNotAssignable( { source: `["foo"]`, @@ -1411,7 +1428,8 @@ describe("compiler: checker: type relations", () => { }); }); - describe("cannot assign a type to a value constraint", () => { + // Disabled for now as this is allowed for transition to value types + describe.skip("cannot assign a type to a value constraint", () => { it.each([ ["{}", "valueof unknown"], ["{}", "valueof {}"], From f7eb99a76a29f7579929e340c1d9e9cc85a972f5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 18 Mar 2024 20:19:04 -0700 Subject: [PATCH 038/184] Fix up issues --- packages/compiler/src/core/checker.ts | 23 ++++++++++++++++-- .../compiler/test/checker/relation.test.ts | 24 +++++++++++++++++++ packages/protobuf/src/proto.ts | 6 +---- .../diagnostics.txt | 4 ++-- .../illegal field reservations/input/main.tsp | 2 +- .../reserved field collisions/input/main.tsp | 2 +- .../scenarios/reserved fields/input/main.tsp | 2 +- 7 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 546615074d..e43c0c55bf 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3927,7 +3927,13 @@ export function createChecker(program: Program): Checker { ...arg, jsValue: resolveDecoratorArgJsValue(arg.value, parameter.type.kind === "Value"), }); - if (!checkArgumentAssignable(arg.value, restType, arg.node!)) { + if ( + !checkArgumentAssignable( + arg.value, + parameter.type.kind === "Value" ? { kind: "Value", target: restType } : restType, + arg.node! + ) + ) { hasError = true; } } @@ -3959,7 +3965,7 @@ export function createChecker(program: Program): Checker { if (isValueType(value) || value.kind === "Model" || value.kind === "Tuple") { const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value); reportCheckerDiagnostics(diagnostics); - return res ?? value; // TODO: can this be a compilerAssert + return res ?? value; } else { return value; } @@ -5914,6 +5920,19 @@ export function createChecker(program: Program): Checker { if (isNullType(source)) { return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); } + if (target.kind === "Union") { + for (const option of target.variants.values()) { + const [related] = isValueOfTypeInternal( + source, + option.type, + diagnosticTarget, + relationCache + ); + if (related) { + return [Related.true, []]; + } + } + } switch (source.kind) { case "ObjectLiteral": diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 43d257e607..915f1ba793 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1379,6 +1379,30 @@ describe("compiler: checker: type relations", () => { }); }); + describe("valueof tuple", () => { + it("can assign tuple literal", async () => { + await expectValueAssignable({ + source: `#["foo", 12]`, + target: "valueof [string, int32]", + }); + }); + }); + + describe("valueof union", () => { + it("can assign tuple literal variant", async () => { + await expectValueAssignable({ + source: `#["foo", 12]`, + target: "valueof ([string, int32] | string | boolean)", + }); + }); + it("can assign string variant", async () => { + await expectValueAssignable({ + source: `"foo"`, + target: "valueof ([string, int32] | string | boolean)", + }); + }); + }); + it("can use valueof in template parameter constraints", async () => { const diagnostics = await runner.diagnose(` model Foo { diff --git a/packages/protobuf/src/proto.ts b/packages/protobuf/src/proto.ts index 249a89f1c1..6459961b4e 100644 --- a/packages/protobuf/src/proto.ts +++ b/packages/protobuf/src/proto.ts @@ -135,11 +135,7 @@ export function $reserve( target: Model, ...reservations: readonly (Type | number | string)[] ) { - const finalReservations = reservations - .map((reservation) => - typeof reservation === "object" ? getTuple(ctx.program, reservation) : reservation - ) - .filter((v) => v != null); + const finalReservations = reservations.filter((v) => v != null); ctx.program.stateMap(state.reserve).set(target, finalReservations); } diff --git a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt index 794e8483e6..f14f8c09d9 100644 --- a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt +++ b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt @@ -1,2 +1,2 @@ - - error @typespec/protobuf/illegal-reservation: reservation value must be a string literal, uint32 literal, or a tuple of two uint32 literals denoting a range - - error @typespec/protobuf/illegal-reservation: reservation value must be a string literal, uint32 literal, or a tuple of two uint32 literals denoting a range +/test/main.tsp:13:34 - error invalid-argument: Argument 'string' is not assignable to parameter of type 'valueof string | [uint32, uint32] | uint32' +/test/main.tsp:13:42 - error invalid-argument: Argument 'uint32' is not assignable to parameter of type 'valueof string | [uint32, uint32] | uint32' diff --git a/packages/protobuf/test/scenarios/illegal field reservations/input/main.tsp b/packages/protobuf/test/scenarios/illegal field reservations/input/main.tsp index c57f720991..bce61b4047 100644 --- a/packages/protobuf/test/scenarios/illegal field reservations/input/main.tsp +++ b/packages/protobuf/test/scenarios/illegal field reservations/input/main.tsp @@ -10,7 +10,7 @@ interface Service { foo(...Input): {}; } -@reserve(2, 15, [9, 11], "foo", string, uint32) +@reserve(2, 15, #[9, 11], "foo", string, uint32) model Input { @field(1) testInputField: string; } diff --git a/packages/protobuf/test/scenarios/reserved field collisions/input/main.tsp b/packages/protobuf/test/scenarios/reserved field collisions/input/main.tsp index 4046c2b39f..7ca8029ab2 100644 --- a/packages/protobuf/test/scenarios/reserved field collisions/input/main.tsp +++ b/packages/protobuf/test/scenarios/reserved field collisions/input/main.tsp @@ -10,7 +10,7 @@ interface Service { foo(...Input): {}; } -@reserve(2, 15, [9, 11], "foo", "bar") +@reserve(2, 15, #[9, 11], "foo", "bar") model Input { @field(1) foo: string; @field(2) field2: int32; diff --git a/packages/protobuf/test/scenarios/reserved fields/input/main.tsp b/packages/protobuf/test/scenarios/reserved fields/input/main.tsp index 2bfbc9e169..7428217c40 100644 --- a/packages/protobuf/test/scenarios/reserved fields/input/main.tsp +++ b/packages/protobuf/test/scenarios/reserved fields/input/main.tsp @@ -10,7 +10,7 @@ interface Service { foo(...Input): {}; } -@reserve(2, 15, [9, 11], "foo", "bar") +@reserve(2, 15, #[9, 11], "foo", "bar") model Input { @field(1) testInputField: string; } From b4f84d76563c59a00f33c2e7a003028cfeadf946 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 19 Mar 2024 08:36:21 -0700 Subject: [PATCH 039/184] Get tuple --- packages/compiler/src/core/checker.ts | 23 +++++++++ packages/compiler/src/core/messages.ts | 6 +++ .../compiler/test/checker/relation.test.ts | 49 ++++++++++++++++++- packages/protobuf/src/proto.ts | 18 ------- 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e43c0c55bf..5859f1badd 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -6135,9 +6135,32 @@ export function createChecker(program: Program): Checker { relationCache ); diagnostics.push(...indexerDiagnostics); + } else { + for (const [propName] of remainingProperties) { + diagnostics.push( + createDiagnostic({ + code: "unexpected-property", + format: { + propertyName: propName, + type: getEntityName(target), + }, + target: getObjectLiteralPropertyNode(source, propName), + }) + ); + } } return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; } + function getObjectLiteralPropertyNode( + object: ObjectLiteral, + propertyName: string + ): DiagnosticTarget { + return ( + object.node.properties.find( + (x) => x.kind === SyntaxKind.ObjectLiteralProperty && x.id.sv === propertyName + ) ?? object.node + ); + } function isTupleLiteralOfArrayType( source: TupleLiteral, diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 2300abdd2a..732f0a241f 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -441,6 +441,12 @@ const diagnostics = { default: paramMessage`Property '${"propertyName"}' is missing on type '${"sourceType"}' but required in '${"targetType"}'`, }, }, + "unexpected-property": { + severity: "error", + messages: { + default: paramMessage`Object literal may only specify known properties, and '${"propertyName"}' does not exist in type '${"type"}'.`, + }, + }, "extends-interface": { severity: "error", messages: { diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 915f1ba793..1e58465dd9 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -67,13 +67,14 @@ describe("compiler: checker: type relations", () => { diagnostics: readonly Diagnostic[]; expectedDiagnosticPos: number; }> { + const cursor = source.includes("┆") ? "" : "┆"; host.addJsFile("mock.js", { $mock: () => null }); const { source: code, pos } = extractCursor(` import "./mock.js"; ${commonCode ?? ""} extern dec mock(target: unknown, target: ${target}); - alias Source = ┆${source}; + alias Source = ${cursor}${source}; `); await runner.compile(code); const alias: AliasStatementNode | undefined = runner.program.sourceFiles @@ -1293,6 +1294,20 @@ describe("compiler: checker: type relations", () => { ); }); + it("emit diagnostic when using extra properties", async () => { + await expectValueNotAssignable( + { + source: `#{name: "foo", ┆notDefined: "bar"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }, + { + code: "unexpected-property", + message: `Object literal may only specify known properties, and 'notDefined' does not exist in type 'Info'.`, + } + ); + }); + it("cannot assign a tuple literal", async () => { await expectValueNotAssignable( { @@ -1386,6 +1401,38 @@ describe("compiler: checker: type relations", () => { target: "valueof [string, int32]", }); }); + + it("cannot assign tuple literal with too few values", async () => { + await expectValueNotAssignable( + { + source: `#["foo"]`, + target: "valueof [string, string]", + }, + { + code: "unassignable", + message: [ + `Type '#["foo"]' is not assignable to type '[string, string]'`, + " Source has 1 element(s) but target requires 2.", + ].join("\n"), + } + ); + }); + + it("cannot assign tuple literal with too many values", async () => { + await expectValueNotAssignable( + { + source: `#["a", "b", "c"]`, + target: "valueof [string, string]", + }, + { + code: "unassignable", + message: [ + `Type '#["a", "b", "c"]' is not assignable to type '[string, string]'`, + " Source has 3 element(s) but target requires 2.", + ].join("\n"), + } + ); + }); }); describe("valueof union", () => { diff --git a/packages/protobuf/src/proto.ts b/packages/protobuf/src/proto.ts index 6459961b4e..f4da13b1a1 100644 --- a/packages/protobuf/src/proto.ts +++ b/packages/protobuf/src/proto.ts @@ -10,12 +10,10 @@ import { Model, ModelProperty, Namespace, - NumericLiteral, Operation, Program, resolvePath, StringLiteral, - Tuple, Type, } from "@typespec/compiler"; @@ -112,22 +110,6 @@ export function $stream(ctx: DecoratorContext, target: Operation, mode: EnumMemb ctx.program.stateMap(state.stream).set(target, emitStreamingMode); } -function getTuple(program: Program, t: Type): [number, number] | null { - if (t.kind !== "Tuple" || t.values.some((v) => v.kind !== "Number") || t.values.length !== 2) { - reportDiagnostic(program, { - code: "illegal-reservation", - target: t, - }); - - return null; - } - - return Object.assign( - (t as Tuple).values.map((v) => (v as NumericLiteral).value) as [number, number], - { type: t } - ); -} - export type Reservation = string | number | ([number, number] & { type: Type }); export function $reserve( From 07f461e403f8214d9cae155d5c36e26f47c45992 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 19 Mar 2024 08:37:56 -0700 Subject: [PATCH 040/184] Additional properties --- packages/compiler/test/checker/relation.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 1e58465dd9..2763d5258f 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1271,6 +1271,14 @@ describe("compiler: checker: type relations", () => { }); }); + it("can assign object literal with additional properties", async () => { + await expectValueAssignable({ + source: `#{age: 21, name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { age: int32, ...Record }`, + }); + }); + it("can assign a model (LEGACY)", async () => { await expectTypeAssignable({ source: `{name: "foo"}`, From 1d9bd2b208b6ff7f059de57d0f65868dc7744875 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 20 Mar 2024 07:59:58 -0700 Subject: [PATCH 041/184] . --- docs/language-basics/type-and-values.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/language-basics/type-and-values.md b/docs/language-basics/type-and-values.md index 6a0597a05d..e14c036858 100644 --- a/docs/language-basics/type-and-values.md +++ b/docs/language-basics/type-and-values.md @@ -20,7 +20,6 @@ TypeSpec has the concept of Types and Values, entities can be either a Type, a V | `Tuple` | ✅ | | | `Enum` | ✅ | | | `EnumMember` | ✅ | ✅ | -| `EnumMember` | ✅ | ✅ | | `StringLiteral` | ✅ | ✅ | | `NumberLiteral` | ✅ | ✅ | | `BooleanLiteral` | ✅ | ✅ | From 856699cd6945eec722ee311c14d4dccbad04cbaa Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 4 Apr 2024 10:24:05 -0700 Subject: [PATCH 042/184] Add support for const in parser --- grammars/typespec.json | 31 ++++++++++- packages/compiler/src/core/parser.ts | 30 ++++++++++ packages/compiler/src/core/scanner.ts | 3 + packages/compiler/src/core/types.ts | 9 +++ .../compiler/src/formatter/print/printer.ts | 15 +++++ packages/compiler/src/server/classify.ts | 3 + packages/compiler/src/server/tmlanguage.ts | 15 ++++- .../compiler/test/formatter/formatter.test.ts | 24 ++++++++ packages/compiler/test/parser.test.ts | 28 +++++++--- .../compiler/test/server/colorization.test.ts | 55 +++++++++++++++---- .../decorators-signatures.ts | 8 ++- 11 files changed, 197 insertions(+), 24 deletions(-) diff --git a/grammars/typespec.json b/grammars/typespec.json index 0cae7d62e1..2b6937a49a 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -62,6 +62,30 @@ "name": "constant.language.tsp", "match": "\\b(true|false)\\b" }, + "const-statement": { + "name": "meta.const-statement.typespec", + "begin": "\\b(const)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "beginCaptures": { + "1": { + "name": "keyword.other.tsp" + }, + "2": { + "name": "variable.name.tsp" + } + }, + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "patterns": [ + { + "include": "#type-annotation" + }, + { + "include": "#operator-assignment" + }, + { + "include": "#expression" + } + ] + }, "decorator": { "name": "meta.decorator.typespec", "begin": "((@)\\b[_$[:alpha:]]([_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)", @@ -724,7 +748,7 @@ "include": "#directive" }, { - "include": "#model-spread-property" + "include": "#spread-operator" }, { "include": "#punctuation-comma" @@ -733,7 +757,7 @@ }, "object-literal-property": { "name": "meta.object-literal-property.typespec", - "begin": "(?:(\\b[_$[:alpha:]][_$[:alnum:]]*\\b)\\s*(:))", + "begin": "(?:(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\s*(:))", "beginCaptures": { "1": { "name": "variable.name.tsp" @@ -1099,6 +1123,9 @@ { "include": "#alias-statement" }, + { + "include": "#const-statement" + }, { "include": "#namespace-statement" }, diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index e9f5716e93..6e7b1aa27e 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -20,6 +20,7 @@ import { BlockComment, BooleanLiteralNode, Comment, + ConstStatementNode, DeclarationNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, @@ -463,6 +464,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "alias statement"); item = parseAliasStatement(pos); break; + case Token.ConstKeyword: + reportInvalidDecorators(decorators, "const statement"); + item = parseConstStatement(pos); + break; case Token.UsingKeyword: reportInvalidDecorators(decorators, "using statement"); item = parseUsingStatement(pos); @@ -1133,6 +1138,29 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseConstStatement(pos: number): ConstStatementNode { + parseExpected(Token.ConstKeyword); + const id = parseIdentifier(); + const type = parseOptionalTypeAnnotation(); + parseExpected(Token.Equals); + const value = parseExpression(); + parseExpected(Token.Semicolon); + return { + kind: SyntaxKind.ConstStatement, + id, + value, + type, + ...finishNode(pos), + }; + } + + function parseOptionalTypeAnnotation(): Expression | undefined { + if (parseOptional(Token.Colon)) { + return parseExpression(); + } + return undefined; + } + function parseExpression(): Expression { return parseUnionExpressionOrHigher(); } @@ -3361,6 +3389,8 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined visitEach(cb, node.templateParameters) || visitNode(cb, node.value) ); + case SyntaxKind.ConstStatement: + return visitNode(cb, node.id) || visitNode(cb, node.value) || visitNode(cb, node.type); case SyntaxKind.DecoratorDeclarationStatement: return ( visitEach(cb, node.modifiers) || diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index d041e64cb0..21f6d7f2f7 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -125,6 +125,7 @@ export enum Token { DecKeyword, FnKeyword, ValueOfKeyword, + ConstKeyword, // Add new statement keyword above /** @internal */ __EndStatementKeyword, @@ -247,6 +248,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.DecKeyword, "'dec'"], [Token.FnKeyword, "'fn'"], [Token.ValueOfKeyword, "'valueof'"], + [Token.ConstKeyword, "'const'"], [Token.ExtendsKeyword, "'extends'"], [Token.TrueKeyword, "'true'"], [Token.FalseKeyword, "'false'"], @@ -277,6 +279,7 @@ export const Keywords: ReadonlyMap = new Map([ ["dec", Token.DecKeyword], ["fn", Token.FnKeyword], ["valueof", Token.ValueOfKeyword], + ["const", Token.ConstKeyword], ["true", Token.TrueKeyword], ["false", Token.FalseKeyword], ["return", Token.ReturnKeyword], diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 082859e5e5..2cab0b7483 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -868,6 +868,7 @@ export enum SyntaxKind { ObjectLiteralProperty, ObjectLiteralSpreadProperty, TupleLiteral, + ConstStatement, } export const enum NodeFlags { @@ -1052,6 +1053,7 @@ export type Statement = | DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode | AugmentDecoratorStatementNode + | ConstStatementNode | EmptyStatementNode | InvalidStatementNode | ProjectionStatementNode; @@ -1276,6 +1278,13 @@ export interface AliasStatementNode extends BaseNode, DeclarationNode, TemplateD readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } +export interface ConstStatementNode extends BaseNode, DeclarationNode { + readonly kind: SyntaxKind.ConstStatement; + readonly value: Expression; + readonly type?: Expression; + readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; +} + export interface InvalidStatementNode extends BaseNode { readonly kind: SyntaxKind.InvalidStatement; readonly decorators: readonly DecoratorExpressionNode[]; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 6f53f247a9..3fd800a62f 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -10,6 +10,7 @@ import { BlockComment, BooleanLiteralNode, Comment, + ConstStatementNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, DirectiveExpressionNode, @@ -384,6 +385,8 @@ export function printNode( ); case SyntaxKind.TupleLiteral: return printTupleLiteral(path as AstPath, options, print); + case SyntaxKind.ConstStatement: + return printConstStatement(path as AstPath, options, print); case SyntaxKind.StringTemplateSpan: case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: @@ -415,6 +418,7 @@ export function printTypeSpecScript( body.push(printStatementSequence(path, options, print, "statements")); return body; } + export function printAliasStatement( path: AstPath, options: TypeSpecPrettierOptions, @@ -425,6 +429,17 @@ export function printAliasStatement( return ["alias ", id, template, " = ", path.call(print, "value"), ";"]; } +export function printConstStatement( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + const node = path.node; + const id = path.call(print, "id"); + const type = node.type ? [": ", path.call(print, "type")] : ""; + return ["const ", id, type, " = ", path.call(print, "value"), ";"]; +} + function printTemplateParameters( path: AstPath, options: TypeSpecPrettierOptions, diff --git a/packages/compiler/src/server/classify.ts b/packages/compiler/src/server/classify.ts index 0657dfbedd..863942df0a 100644 --- a/packages/compiler/src/server/classify.ts +++ b/packages/compiler/src/server/classify.ts @@ -240,6 +240,9 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] { case SyntaxKind.FunctionDeclarationStatement: classify(node.id, SemanticTokenKind.Function); break; + case SyntaxKind.ConstStatement: + classify(node.id, SemanticTokenKind.Variable); + break; case SyntaxKind.FunctionParameter: classify(node.id, SemanticTokenKind.Parameter); break; diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index 207c550d31..f02f7420cd 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -478,6 +478,7 @@ const objectLiteralProperty: BeginEndRule = { end: universalEnd, patterns: [token, expression], }; + const objectLiteral: BeginEndRule = { key: "object-literal", scope: meta, @@ -489,7 +490,7 @@ const objectLiteral: BeginEndRule = { endCaptures: { "0": { scope: "punctuation.curlybrace.close.tsp" }, }, - patterns: [token, objectLiteralProperty, directive, modelSpreadProperty, punctuationComma], + patterns: [token, objectLiteralProperty, directive, spreadExpression, punctuationComma], }; const modelHeritage: BeginEndRule = { @@ -632,6 +633,17 @@ const aliasStatement: BeginEndRule = { end: universalEnd, patterns: [typeParameters, operatorAssignment, expression], }; +const constStatement: BeginEndRule = { + key: "const-statement", + scope: meta, + begin: `\\b(const)\\b\\s+(${identifier})`, + beginCaptures: { + "1": { scope: "keyword.other.tsp" }, + "2": { scope: "variable.name.tsp" }, + }, + end: universalEnd, + patterns: [typeAnnotation, operatorAssignment, expression], +}; const namespaceName: BeginEndRule = { key: "namespace-name", @@ -981,6 +993,7 @@ statement.patterns = [ interfaceStatement, enumStatement, aliasStatement, + constStatement, namespaceStatement, operationStatement, importStatement, diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index c1117f9477..ebc08756c4 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -2807,4 +2807,28 @@ alias T = """ }); }); }); + + describe("const", () => { + it("format const without type annotations", async () => { + await assertFormat({ + code: ` +const a = 123; +`, + expected: ` +const a = 123; +`, + }); + }); + + it("format const with type annotations", async () => { + await assertFormat({ + code: ` +const a : in32= 123; +`, + expected: ` +const a: in32 = 123; +`, + }); + }); + }); }); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 02993dc3b4..56eec40bf1 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -223,20 +223,34 @@ describe("compiler: parser", () => { parseErrorEach([['union A { @myDec "x" x: number, y: string }', [/';' expected/]]]); }); + describe("const statements", () => { + parseEach([ + `const a = 123;`, + `const a: Info = 123;`, + `const a: {inline: string} = #{inline: "abc"};`, + `const a: string | int32 = int32;`, + ]); + parseErrorEach([ + [`const = 123;`, [/Identifier expected/]], + [`const a`, [{ message: "'=' expected." }]], + [`const a =`, [/Expression expected./]], + ]); + }); + describe("object literals", () => { parseEach([ - `alias A = #{a: "abc"};`, - `alias A = #{a: "abc", b: "def"};`, - `alias A = #{a: "abc", ...B};`, - `alias A = #{a: "abc", ...B, c: "ghi"};`, + `const A = #{a: "abc"};`, + `const A = #{a: "abc", b: "def"};`, + `const A = #{a: "abc", ...B};`, + `const A = #{a: "abc", ...B, c: "ghi"};`, ]); }); describe("tuple literals", () => { parseEach([ - `alias A = #["abc"];`, - `alias A = #["abc", 123];`, - `alias A = #["abc", 123, #{nested: true}];`, + `const A = #["abc"];`, + `const A = #["abc", 123];`, + `const A = #["abc", 123, #{nested: true}];`, ]); }); diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 3f191d1131..e5f4a6d2e8 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -50,6 +50,7 @@ const Token = { to: createToken("to", "keyword.other.tsp"), from: createToken("from", "keyword.other.tsp"), valueof: createToken("valueof", "keyword.other.tsp"), + const: createToken("const", "keyword.other.tsp"), other: (text: string) => createToken(text, "keyword.other.tsp"), }, @@ -1062,14 +1063,40 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); + describe("const", () => { + it("without type annotation", async () => { + const tokens = await tokenize("const foo = 123;"); + deepStrictEqual(tokens, [ + Token.keywords.const, + Token.identifiers.variable("foo"), + Token.operators.assignment, + Token.literals.numeric("123"), + Token.punctuation.semicolon, + ]); + }); + + it("with type annotation", async () => { + const tokens = await tokenize("const foo: int32 = 123;"); + deepStrictEqual(tokens, [ + Token.keywords.const, + Token.identifiers.variable("foo"), + Token.operators.typeAnnotation, + Token.identifiers.type("int32"), + Token.operators.assignment, + Token.literals.numeric("123"), + Token.punctuation.semicolon, + ]); + }); + }); + describe("object literals", () => { it("empty", async () => { - const tokens = await tokenizeWithAlias("#{}"); + const tokens = await tokenizeWithConst("#{}"); deepStrictEqual(tokens, [Token.punctuation.openHashBrace, Token.punctuation.closeBrace]); }); it("single prop", async () => { - const tokens = await tokenizeWithAlias(`#{name: "John"}`); + const tokens = await tokenizeWithConst(`#{name: "John"}`); deepStrictEqual(tokens, [ Token.punctuation.openHashBrace, Token.identifiers.variable("name"), @@ -1080,7 +1107,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); it("multiple prop", async () => { - const tokens = await tokenizeWithAlias(`#{name: "John", age: 21}`); + const tokens = await tokenizeWithConst(`#{name: "John", age: 21}`); deepStrictEqual(tokens, [ Token.punctuation.openHashBrace, Token.identifiers.variable("name"), @@ -1095,7 +1122,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); it("spreading prop", async () => { - const tokens = await tokenizeWithAlias(`#{name: "John", ...Common}`); + const tokens = await tokenizeWithConst(`#{name: "John", ...Common}`); deepStrictEqual(tokens, [ Token.punctuation.openHashBrace, Token.identifiers.variable("name"), @@ -1109,7 +1136,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); it("nested prop", async () => { - const tokens = await tokenizeWithAlias(`#{prop: #{age: 21}}`); + const tokens = await tokenizeWithConst(`#{prop: #{age: 21}}`); deepStrictEqual(tokens, [ Token.punctuation.openHashBrace, Token.identifiers.variable("prop"), @@ -1126,7 +1153,7 @@ function testColorization(description: string, tokenize: Tokenize) { describe("tuple literals", () => { it("empty", async () => { - const tokens = await tokenizeWithAlias("#[]"); + const tokens = await tokenizeWithConst("#[]"); deepStrictEqual(tokens, [ Token.punctuation.openHashBracket, Token.punctuation.closeBracket, @@ -1134,7 +1161,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); it("single value", async () => { - const tokens = await tokenizeWithAlias(`#["John"]`); + const tokens = await tokenizeWithConst(`#["John"]`); deepStrictEqual(tokens, [ Token.punctuation.openHashBracket, Token.literals.stringQuoted("John"), @@ -1143,7 +1170,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); it("multiple values", async () => { - const tokens = await tokenizeWithAlias(`#["John", 21]`); + const tokens = await tokenizeWithConst(`#["John", 21]`); deepStrictEqual(tokens, [ Token.punctuation.openHashBracket, Token.literals.stringQuoted("John"), @@ -1154,7 +1181,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); it("nested tuple", async () => { - const tokens = await tokenizeWithAlias(`#[#[21]]`); + const tokens = await tokenizeWithConst(`#[#[21]]`); deepStrictEqual(tokens, [ Token.punctuation.openHashBracket, Token.punctuation.openHashBracket, @@ -1512,9 +1539,13 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); - async function tokenizeWithAlias(text: string) { - const common = [Token.keywords.alias, Token.identifiers.type("T"), Token.operators.assignment]; - const tokens = await tokenize(`alias T = ${text}`); + async function tokenizeWithConst(text: string) { + const common = [ + Token.keywords.const, + Token.identifiers.variable("a"), + Token.operators.assignment, + ]; + const tokens = await tokenize(`const a = ${text}`); for (let i = 0; i < common.length; i++) { deepStrictEqual(tokens[i], common[i]); } diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index 7b52f11b36..98df43e0a2 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -3,6 +3,7 @@ import { FunctionParameter, IntrinsicScalarName, Model, + ParamConstraintUnion, Program, Scalar, SyntaxKind, @@ -114,7 +115,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } } - function getRestTSParmeterType(type: Type | ValueType) { + function getRestTSParmeterType(type: Type | ValueType | ParamConstraintUnion) { if (type.kind === "Value") { if (type.target.kind === "Model" && isArrayModelType(program, type.target)) { return `(${getValueTSType(type.target.indexer.value)})[]`; @@ -129,7 +130,10 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat return `${getTSParmeterType(type.indexer.value)}[]`; } - function getTSParmeterType(type: Type | ValueType, isTarget?: boolean): string { + function getTSParmeterType( + type: Type | ValueType | ParamConstraintUnion, + isTarget?: boolean + ): string { if (type.kind === "Value") { return getValueTSType(type.target); } From 6dead0d39ceef25201d4bbdc6a78b99327703a5c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 09:14:49 -0700 Subject: [PATCH 043/184] Introduce Value entities --- packages/compiler/src/core/binder.ts | 8 + packages/compiler/src/core/checker.ts | 572 ++++++++++++------ packages/compiler/src/core/diagnostics.ts | 4 +- .../src/core/helpers/type-name-utils.ts | 55 +- packages/compiler/src/core/js-marshaller.ts | 58 +- packages/compiler/src/core/messages.ts | 6 + packages/compiler/src/core/program.ts | 4 +- packages/compiler/src/core/projector.ts | 12 +- packages/compiler/src/core/type-utils.ts | 44 +- packages/compiler/src/core/types.ts | 146 +++-- .../src/emitter-framework/type-emitter.ts | 3 + packages/compiler/test/checker/model.test.ts | 9 +- packages/compiler/test/checker/scalar.test.ts | 43 +- packages/compiler/test/checker/values.test.ts | 82 +-- .../projection/projector-identity.test.ts | 16 +- packages/protobuf/src/transform/index.ts | 8 +- .../tspd/src/ref-doc/emitters/markdown.ts | 4 +- packages/versioning/src/validate.ts | 4 +- 18 files changed, 676 insertions(+), 402 deletions(-) diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index bfbbe8df5c..7d0b5fd361 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -5,6 +5,7 @@ import { visitChildren } from "./parser.js"; import { Program } from "./program.js"; import { AliasStatementNode, + ConstStatementNode, Declaration, DecoratorDeclarationStatementNode, EnumStatementNode, @@ -271,6 +272,9 @@ export function createBinder(program: Program): Binder { case SyntaxKind.AliasStatement: bindAliasStatement(node); break; + case SyntaxKind.ConstStatement: + bindConstStatement(node); + break; case SyntaxKind.EnumStatement: bindEnumStatement(node); break; @@ -473,6 +477,9 @@ export function createBinder(program: Program): Binder { // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } + function bindConstStatement(node: ConstStatementNode) { + declareSymbol(node, SymbolFlags.Const); + } function bindEnumStatement(node: EnumStatementNode) { declareSymbol(node, SymbolFlags.Enum); @@ -603,6 +610,7 @@ function hasScope(node: Node): node is ScopeNode { switch (node.kind) { case SyntaxKind.ModelStatement: case SyntaxKind.ScalarStatement: + case SyntaxKind.ConstStatement: case SyntaxKind.AliasStatement: case SyntaxKind.TypeSpecScript: case SyntaxKind.InterfaceStatement: diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index fa7d842f85..677f362529 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -22,6 +22,7 @@ import { } from "./helpers/index.js"; import { marshallTypeForJSWithLegacyCast, tryMarshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; +import { Numeric } from "./numeric.js"; import { exprIsBareIdentifier, getIdentifierContext, @@ -35,21 +36,23 @@ import { getParentTemplateNode, isErrorType, isNeverType, - isNullType, isTemplateInstance, + isType, isUnknownType, - isValueOnly, - isValueType, + isValue, isVoidType, } from "./type-utils.js"; import { AliasStatementNode, ArrayExpressionNode, ArrayModelType, + ArrayValue, AugmentDecoratorStatementNode, BooleanLiteral, BooleanLiteralNode, + BooleanValue, CodeFix, + ConstStatementNode, DecoratedType, Decorator, DecoratorApplication, @@ -100,8 +103,10 @@ import { NodeFlags, NumericLiteral, NumericLiteralNode, - ObjectLiteral, + NumericValue, ObjectLiteralNode, + ObjectValue, + ObjectValuePropertyDescriptor, Operation, OperationStatementNode, ParamConstraintUnion, @@ -140,6 +145,7 @@ import { StringTemplateSpanLiteral, StringTemplateSpanValue, StringTemplateTailNode, + StringValue, Sym, SymbolFlags, SymbolLinks, @@ -153,7 +159,6 @@ import { TemplatedType, Tuple, TupleExpressionNode, - TupleLiteral, TupleLiteralNode, Type, TypeInstantiationMap, @@ -658,15 +663,52 @@ export function createChecker(program: Program): Checker { } function getTypeForNode(node: Node, mapper?: TypeMapper): Type { - const type = getTypeOrValueForNode(node, mapper); - if (isValueOnly(type)) { - reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + const typeOrValue = getTypeOrValueForNode(node, mapper, undefined); + if (isValue(typeOrValue)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "value-in-type", + target: node, + }) + ); return errorType; } - return type; + return typeOrValue; } - function getTypeOrValueForNode(node: Node, mapper?: TypeMapper): Type | Value { + function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | undefined { + switch (node.kind) { + case SyntaxKind.ObjectLiteral: + return checkObjectLiteral(node, mapper); + case SyntaxKind.TupleLiteral: + return checkTupleLiteral(node, mapper); + case SyntaxKind.ConstStatement: + return checkConst(node); + case SyntaxKind.StringLiteral: + return checkStringValue(node); + case SyntaxKind.NumericLiteral: + return checkNumericValue(node); + case SyntaxKind.BooleanLiteral: + return checkBooleanValue(node); + case SyntaxKind.TypeReference: + return checkValueReference(node, mapper); + default: + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: "?" }, // TODO: better message + target: node, + }) + ); + return undefined; + } + } + + function getTypeOrValueForNode( + node: Node, + mapper?: TypeMapper, + constraint?: Type | ValueType | ParamConstraintUnion | undefined + ): Type | Value { switch (node.kind) { case SyntaxKind.ModelExpression: return checkModel(node, mapper); @@ -694,13 +736,13 @@ export function createChecker(program: Program): Checker { case SyntaxKind.OperationStatement: return checkOperation(node, mapper); case SyntaxKind.NumericLiteral: - return checkNumericLiteral(node); + return constraint?.kind === "Value" ? checkNumericValue(node) : checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: - return checkBooleanLiteral(node); + return constraint?.kind === "Value" ? checkBooleanValue(node) : checkBooleanLiteral(node); + case SyntaxKind.StringLiteral: + return constraint?.kind === "Value" ? checkStringValue(node) : checkStringLiteral(node); case SyntaxKind.TupleExpression: return checkTupleExpression(node, mapper); - case SyntaxKind.StringLiteral: - return checkStringLiteral(node); case SyntaxKind.StringTemplateExpression: return checkStringTemplateExpresion(node, mapper); case SyntaxKind.ArrayExpression: @@ -714,7 +756,7 @@ export function createChecker(program: Program): Checker { case SyntaxKind.FunctionDeclarationStatement: return checkFunctionDeclaration(node, mapper); case SyntaxKind.TypeReference: - return checkTypeReference(node, mapper); + return checkTypeOrValueReference(node, mapper); case SyntaxKind.TemplateArgument: return checkTemplateArgument(node, mapper); case SyntaxKind.TemplateParameterDeclaration: @@ -731,14 +773,11 @@ export function createChecker(program: Program): Checker { return checkObjectLiteral(node, mapper); case SyntaxKind.TupleLiteral: return checkTupleLiteral(node, mapper); + case SyntaxKind.ConstStatement: + return checkConst(node) ?? errorType; // TODO: do we want that? + default: return errorType; } - - // we don't emit an error here as we blindly call this function - // with any node type, but some nodes don't produce a type - // (e.g. imports). errorType should result in an error if it - // bubbles out somewhere its not supposed to be. - return errorType; } /** @@ -749,6 +788,7 @@ export function createChecker(program: Program): Checker { | ModelStatementNode | ScalarStatementNode | AliasStatementNode + | ConstStatementNode | InterfaceStatementNode | OperationStatementNode | TemplateParameterDeclarationNode @@ -861,7 +901,7 @@ export function createChecker(program: Program): Checker { if (declaredType.default === undefined) { return undefined; } - if (isErrorType(declaredType.default)) { + if (isType(declaredType.default) && isErrorType(declaredType.default)) { return declaredType.default; } @@ -873,11 +913,11 @@ export function createChecker(program: Program): Checker { templateParameters: readonly TemplateParameterDeclarationNode[], index: number, constraint: Entity | undefined - ) { + ): Type | Value { function visit(node: Node) { const type = getTypeOrValueForNode(node); let hasError = false; - if (type.kind === "TemplateParameter") { + if ("kind" in type && type.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { if (type.node.symbol === templateParameters[i].symbol) { reportCheckerDiagnostic( @@ -900,7 +940,7 @@ export function createChecker(program: Program): Checker { } const type = visit(nodeDefault) ?? errorType; - if (!isErrorType(type) && constraint) { + if (!("kind" in type && isErrorType(type)) && constraint) { checkTypeAssignable(type, constraint, nodeDefault); } return type; @@ -927,6 +967,26 @@ export function createChecker(program: Program): Checker { return type; } + /** + * Check and resolve a type for the given type reference node. + * @param node Node. + * @param mapper Type mapper for template instantiation context. + * @param instantiateTemplate If templated type should be instantiated if they haven't yet. + * @returns Resolved type. + */ + function checkTypeOrValueReference( + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + mapper: TypeMapper | undefined, + instantiateTemplate = true + ): Type | Value { + const sym = resolveTypeReferenceSym(node, mapper); + if (!sym) { + return errorType; + } + + return checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplate) ?? errorType; + } + function checkTemplateArgument( node: TemplateArgumentNode, mapper: TypeMapper | undefined @@ -1166,7 +1226,7 @@ export function createChecker(program: Program): Checker { commit(param, effectiveType); continue; } - } else if (isErrorType(type)) { + } else if ("kind" in type && isErrorType(type)) { // If we got an error type we don't want to keep passing it through so we reduce to unknown // Similar to the above where if the type is not assignable to the constraint we reduce to the constraint commit(param, unknownType); @@ -1178,6 +1238,24 @@ export function createChecker(program: Program): Checker { return finalMap; } + function checkValueReferenceSymbol( + sym: Sym, + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + mapper: TypeMapper | undefined + ): Value | undefined { + // TODO: use common checkTypeOrValueReferenceSymbol + if (sym.flags & SymbolFlags.Const) { + return getValueForNode(sym.declarations[0], mapper); + } + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: sym.name }, + target: node, + }) + ); + return undefined; + } /** * Check and resolve the type for the given symbol + node. * @param sym Symbol @@ -1192,6 +1270,30 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined, instantiateTemplates = true ): Type { + // TODO: do we even need this + if (sym.flags & SymbolFlags.Const) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return errorType; + } + + const result = checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplates); + if (result === undefined || isValue(result)) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return errorType; + } + return result; + } + + function checkTypeOrValueReferenceSymbol( + sym: Sym, + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + mapper: TypeMapper | undefined, + instantiateTemplates = true + ): Type | Value | undefined { + if (sym.flags & SymbolFlags.Const) { + return getValueForNode(sym.declarations[0], mapper); + } + if (sym.flags & SymbolFlags.Decorator) { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-type-ref", messageId: "decorator", target: sym }) @@ -1274,7 +1376,7 @@ export function createChecker(program: Program): Checker { } if (sym.flags & SymbolFlags.LateBound) { - compilerAssert(sym.type, "Expected late bound symbol to have type"); + compilerAssert(sym.type, `Expected late bound symbol to have type`); return sym.type; } else if (sym.flags & SymbolFlags.TemplateParameter) { const mapped = checkTemplateParameterDeclaration( @@ -2922,7 +3024,7 @@ export function createChecker(program: Program): Checker { } // Some of the mapper args are still template parameter so we shouldn't create the type. - return mapper.args.every((t) => t.kind !== "TemplateParameter"); + return mapper.args.every((t) => isValue(t) || t.kind !== "TemplateParameter"); } function checkModelExpression(node: ModelExpressionNode, mapper: TypeMapper | undefined) { @@ -3046,25 +3148,26 @@ export function createChecker(program: Program): Checker { function checkObjectLiteral( node: ObjectLiteralNode, mapper: TypeMapper | undefined - ): ObjectLiteral { + ): ObjectValue { return { - kind: "ObjectLiteral", + valueKind: "ObjectValue", node: node, properties: checkObjectLiteralProperties(node, mapper), + type: null as any, // TODO: fix }; } function checkObjectLiteralProperties( node: ObjectLiteralNode, mapper: TypeMapper | undefined - ): Map { - const properties = new Map(); + ): Map { + const properties = new Map(); for (const prop of node.properties!) { if ("id" in prop) { - const type = getTypeOrValueForNode(prop.value, mapper); - if (checkIsValue(type, prop.value)) { - properties.set(prop.id.sv, type); + const value = getValueForNode(prop.value, mapper); + if (value !== undefined) { + properties.set(prop.id.sv, { name: prop.id.sv, value: value }); } } else { const targetType = checkObjectSpreadProperty(prop.target, mapper); @@ -3079,7 +3182,7 @@ export function createChecker(program: Program): Checker { } function checkIsValue(type: Type | Value, diagnosticTarget: DiagnosticTarget): type is Value { - if (!isValueType(type)) { + if (!isValue(type)) { reportCheckerDiagnostic( createDiagnostic({ code: "expect-value", @@ -3092,24 +3195,36 @@ export function createChecker(program: Program): Checker { return true; } + function checkIsType(type: Type | Value, diagnosticTarget: DiagnosticTarget): type is Type { + if (!isType(type)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "value-in-type", + target: diagnosticTarget, + }) + ); + return false; + } + return true; + } + function checkObjectSpreadProperty( targetNode: TypeReferenceNode, mapper: TypeMapper | undefined - ): ObjectLiteral | undefined { - const targetType = getTypeOrValueForNode(targetNode, mapper); - - if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) { + ): ObjectValue | undefined { + const value = getValueForNode(targetNode, mapper); + if (value === undefined) { return undefined; } - if (targetType.kind !== "ObjectLiteral") { + if (value.valueKind !== "ObjectValue") { reportCheckerDiagnostic(createDiagnostic({ code: "spread-object", target: targetNode })); return undefined; } - return targetType; + return value; } - function checkTupleLiteral(node: TupleLiteralNode, mapper: TypeMapper | undefined): TupleLiteral { + function checkTupleLiteral(node: TupleLiteralNode, mapper: TypeMapper | undefined): ArrayValue { const values = node.values .map((itemNode) => { const type = getTypeOrValueForNode(itemNode, mapper); @@ -3121,12 +3236,60 @@ export function createChecker(program: Program): Checker { }) .filter(isDefined); return { - kind: "TupleLiteral", + valueKind: "ArrayValue", node: node, values, + type: null as any, // TODO: fix }; } + function checkStringValue(node: StringLiteralNode): StringValue { + return { + valueKind: "StringValue", + value: node.value, + type: getLiteralType(node), + scalar: undefined, + }; + } + + function checkNumericValue(node: NumericLiteralNode): NumericValue { + return { + valueKind: "NumericValue", + value: Numeric(node.valueAsString), + type: getLiteralType(node), + scalar: undefined, + }; + } + + function checkBooleanValue(node: BooleanLiteralNode): BooleanValue { + return { + valueKind: "BooleanValue", + value: node.value, + type: getLiteralType(node), + scalar: undefined, + }; + } + + /** + * Check and resolve a type for the given type reference node. + * @param node Node. + * @param mapper Type mapper for template instantiation context. + * @param instantiateTemplate If templated type should be instantiated if they haven't yet. + * @returns Resolved type. + */ + function checkValueReference( + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + mapper: TypeMapper | undefined + ): Value | undefined { + const sym = resolveTypeReferenceSym(node, mapper); + if (!sym) { + return undefined; + } + + const value = checkValueReferenceSymbol(sym, node, mapper); + return value; + } + function createUnion(options: Type[]): Union { const variants = createRekeyableMap(); const union: Union = createAndFinishType({ @@ -3711,7 +3874,7 @@ export function createChecker(program: Program): Checker { } else { pendingResolutions.start(symId, ResolutionKind.Type); type.type = getTypeForNode(prop.value, mapper); - type.default = prop.default && checkDefault(prop.default, type.type); + type.default = prop.default && (checkDefault(prop.default, type.type) as any); // TODO: fix; if (links) { linkType(links, type, mapper); } @@ -3750,24 +3913,27 @@ export function createChecker(program: Program): Checker { } function isDefaultValue(type: Type | Value): boolean { - if (type.kind === "UnionVariant") { - return isValueType(type.type); - } - if (type.kind === "Tuple") { - reportCheckerDiagnostic( - createDiagnostic({ - code: "deprecated", - codefixes: [createTupleToLiteralCodeFix(type.node)], - format: { - message: - "Using a tuple as a default value is deprecated. Use a tuple literal instead. `#[]`", - }, - target: type.node, - }) - ); - return true; + if (isType(type)) { + if (type.kind === "UnionVariant") { + return isValue(type.type); + } + if (type.kind === "Tuple") { + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createTupleToLiteralCodeFix(type.node)], + format: { + message: + "Using a tuple as a default value is deprecated. Use a tuple literal instead. `#[]`", + }, + target: type.node, + }) + ); + return true; + } } - return isValueType(type); + + return isValue(type); } function checkDefault(defaultNode: Node, type: Type): Type | Value { @@ -3779,13 +3945,14 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic( createDiagnostic({ code: "unsupported-default", - format: { type: defaultType.kind }, + // TODO: fix this + format: { type: (defaultType as any).kind }, target: defaultNode, }) ); return errorType; } - const [related, diagnostics] = isValueType(defaultType) + const [related, diagnostics] = isValue(defaultType) ? isValueOfType(defaultType, type, defaultNode) : isTypeAssignableTo(defaultType, type, defaultNode); if (!related) { @@ -3824,7 +3991,6 @@ export function createChecker(program: Program): Checker { const symbolLinks = getSymbolLinks(sym); - let args = checkDecoratorArguments(decNode, mapper); let hasError = false; if (symbolLinks.declaredType === undefined) { const decoratorDeclNode: DecoratorDeclarationStatementNode | undefined = @@ -3838,13 +4004,20 @@ export function createChecker(program: Program): Checker { } if (symbolLinks.declaredType) { compilerAssert( - symbolLinks.declaredType.kind === ("Decorator" as const), + symbolLinks.declaredType.kind === "Decorator", "Expected to find a decorator type." ); - // Means we have a decorator declaration. - [hasError, args] = checkDecoratorUsage(targetType, symbolLinks.declaredType, args, decNode); + if (!checkDecoratorTarget(targetType, symbolLinks.declaredType, decNode)) { + hasError = true; + } } - if (hasError) { + const [argsHaveError, args] = checkDecoratorArguments( + decNode, + mapper, + symbolLinks.declaredType + ); + + if (hasError || argsHaveError) { return undefined; } return { @@ -3855,16 +4028,11 @@ export function createChecker(program: Program): Checker { }; } - function checkDecoratorUsage( - targetType: Type, - declaration: Decorator, - args: DecoratorArgument[], - decoratorNode: Node - ): [boolean, DecoratorArgument[]] { - let hasError = false; + /** Check the decorator target is valid */ + + function checkDecoratorTarget(targetType: Type, declaration: Decorator, decoratorNode: Node) { const [targetValid] = isTypeAssignableTo(targetType, declaration.target.type, decoratorNode); if (!targetValid) { - hasError = true; reportCheckerDiagnostic( createDiagnostic({ code: "decorator-wrong-target", @@ -3878,15 +4046,43 @@ export function createChecker(program: Program): Checker { }) ); } - const minArgs = declaration.parameters.filter((x) => !x.optional && !x.rest).length; + return targetValid; + } + + function checkDecoratorArguments( + node: DecoratorExpressionNode | AugmentDecoratorStatementNode, + mapper: TypeMapper | undefined, + declaration: Decorator | undefined + ): [boolean, DecoratorArgument[]] { + // if we don't have a declaration we can just return the types or values if + if (declaration === undefined) { + return [ + false, + node.arguments.map((argNode): DecoratorArgument => { + const type = getTypeOrValueForNode(argNode, mapper); + return { + value: type, + jsValue: type, + node: argNode, + }; + }), + ]; + } + + let hasError = false; + + const minArgs = declaration.parameters.filter((x) => !x.optional && !x.rest).length ?? 0; const maxArgs = declaration.parameters[declaration.parameters.length - 1]?.rest ? undefined : declaration.parameters.length; - if (args.length < minArgs || (maxArgs !== undefined && args.length > maxArgs)) { + if ( + node.arguments.length < minArgs || + (maxArgs !== undefined && node.arguments.length > maxArgs) + ) { // In the case we have too little args then this decorator is not applicable. // If there is too many args then we can still run the decorator as long as the args are valid. - if (args.length < minArgs) { + if (node.arguments.length < minArgs) { hasError = true; } @@ -3895,8 +4091,8 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "invalid-argument-count", messageId: "atLeast", - format: { actual: args.length.toString(), expected: minArgs.toString() }, - target: decoratorNode, + format: { actual: node.arguments.length.toString(), expected: minArgs.toString() }, + target: node, }) ); } else { @@ -3904,14 +4100,15 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-argument-count", - format: { actual: args.length.toString(), expected }, - target: decoratorNode, + format: { actual: node.arguments.length.toString(), expected }, + target: node, }) ); } } const resolvedArgs: DecoratorArgument[] = []; + for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { const restType = @@ -3921,20 +4118,24 @@ export function createChecker(program: Program): Checker { parameter.type.kind === "Value" ? parameter.type.target : parameter.type ); if (restType) { - for (let i = index; i < args.length; i++) { - const arg = args[i]; - if (arg && arg.value) { - resolvedArgs.push({ - ...arg, - jsValue: resolveDecoratorArgJsValue(arg.value, parameter.type.kind === "Value"), - }); + for (let i = index; i < node.arguments.length; i++) { + const argNode = node.arguments[i]; + if (argNode) { + const arg = getTypeOrValueForNode(argNode, mapper, parameter.type); + if ( - !checkArgumentAssignable( - arg.value, + checkArgumentAssignable( + arg, parameter.type.kind === "Value" ? { kind: "Value", target: restType } : restType, - arg.node! + argNode ) ) { + resolvedArgs.push({ + value: arg, + node: argNode, + jsValue: resolveDecoratorArgJsValue(arg, parameter.type.kind === "Value"), + }); + } else { hasError = true; } } @@ -3942,12 +4143,14 @@ export function createChecker(program: Program): Checker { } break; } - const arg = args[index]; - if (arg && arg.value) { - if (checkArgumentAssignable(arg.value, parameter.type, arg.node!)) { + const argNode = node.arguments[index]; + if (argNode) { + const arg = getTypeOrValueForNode(argNode, mapper, parameter.type); + if (checkArgumentAssignable(arg, parameter.type, argNode)) { resolvedArgs.push({ - ...arg, - jsValue: resolveDecoratorArgJsValue(arg.value, parameter.type.kind === "Value"), + value: arg, + node: argNode, + jsValue: resolveDecoratorArgJsValue(arg, parameter.type.kind === "Value"), }); } else { hasError = true; @@ -3963,7 +4166,7 @@ export function createChecker(program: Program): Checker { function resolveDecoratorArgJsValue(value: Type | Value, valueOf: boolean) { if (valueOf) { - if (isValueType(value) || value.kind === "Model" || value.kind === "Tuple") { + if (isValue(value) || value.kind === "Model" || value.kind === "Tuple") { const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value); reportCheckerDiagnostics(diagnostics); return res ?? value; @@ -4044,20 +4247,6 @@ export function createChecker(program: Program): Checker { return decorators; } - function checkDecoratorArguments( - decorator: DecoratorExpressionNode | AugmentDecoratorStatementNode, - mapper: TypeMapper | undefined - ): DecoratorArgument[] { - return decorator.arguments.map((argNode): DecoratorArgument => { - const type = getTypeOrValueForNode(argNode, mapper); - return { - value: type, - jsValue: type, - node: argNode, - }; - }); - } - function checkScalar(node: ScalarStatementNode, mapper: TypeMapper | undefined): Scalar { const links = getSymbolLinks(node.symbol); @@ -4144,7 +4333,7 @@ export function createChecker(program: Program): Checker { return extendsType; } - function checkAlias(node: AliasStatementNode, mapper: TypeMapper | undefined): Type | Value { + function checkAlias(node: AliasStatementNode, mapper: TypeMapper | undefined): Type { const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { @@ -4168,8 +4357,8 @@ export function createChecker(program: Program): Checker { } pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = getTypeOrValueForNode(node.value, mapper); - if (!isValueOnly(type)) { + const type = getTypeForNode(node.value, mapper); + if (!isValue(type)) { linkType(links, type, mapper); } pendingResolutions.finish(aliasSymId, ResolutionKind.Type); @@ -4177,6 +4366,30 @@ export function createChecker(program: Program): Checker { return type; } + function checkConst(node: ConstStatementNode): Value | undefined { + const type = node.type ? getTypeForNode(node.type, undefined) : undefined; + + const symId = getSymbolId(node.symbol); + if (pendingResolutions.has(symId, ResolutionKind.Value)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "circular-const", + format: { name: node.id.sv }, + target: node, + }) + ); + return undefined; + } + + pendingResolutions.start(symId, ResolutionKind.Value); + const value = getValueForNode(node.value, undefined); + pendingResolutions.finish(symId, ResolutionKind.Value); + if (value === undefined) { + return undefined; + } + return type ? { ...value, type } : { ...value }; + } + function checkEnum(node: EnumStatementNode, mapper: TypeMapper | undefined): Type { const links = getSymbolLinks(node.symbol); if (!links.type) { @@ -5701,7 +5914,13 @@ export function createChecker(program: Program): Checker { relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { // BACKCOMPAT: Added May 2023 sprint, to be removed by June 2023 sprint - if (source.kind === "TemplateParameter" && source.constraint && target.kind === "Value") { + if ( + "kind" in source && + "kind" in target && + source.kind === "TemplateParameter" && + source.constraint && + target.kind === "Value" + ) { const [assignable] = isTypeAssignableToInternal( source.constraint, target.target, @@ -5721,15 +5940,19 @@ export function createChecker(program: Program): Checker { } } - while (source.kind === "TemplateParameter" && source.constraint !== source) { + while ( + "kind" in source && + source.kind === "TemplateParameter" && + source.constraint !== source + ) { source = source.constraint ?? unknownType; } if (source === target) return [Related.true, []]; - if (target.kind === "Value") { + if ("kind" in target && target.kind === "Value") { return isAssignableToValueType(source, target, diagnosticTarget, relationCache); } - if (isValueOnly(target)) { + if (isValue(target)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } if (target.kind === "ParamConstraintUnion") { @@ -5741,7 +5964,7 @@ export function createChecker(program: Program): Checker { ); } - if (source.kind === "Value" || source.kind === "ParamConstraintUnion" || isValueOnly(source)) { + if (isValue(source) || source.kind === "Value" || source.kind === "ParamConstraintUnion") { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } const isSimpleTypeRelated = isSimpleTypeAssignableTo(source, target); @@ -5830,7 +6053,8 @@ export function createChecker(program: Program): Checker { diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if (source.kind === "Value") { + const isSourceAType = "kind" in source; + if (isSourceAType && source.kind === "Value") { return isTypeAssignableToInternal( source.target, target.target, @@ -5841,6 +6065,7 @@ export function createChecker(program: Program): Checker { // LEGACY BEHAVIOR - Goal here is to all models instead of object literal and tuple instead of tuple literals to get a smooth migration of decorators if ( + isSourceAType && source.kind === "Tuple" && isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === Related.true @@ -5858,6 +6083,7 @@ export function createChecker(program: Program): Checker { ); return [Related.true, []]; } else if ( + isSourceAType && source.kind === "Model" && source.node?.kind === SyntaxKind.ModelExpression && isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === @@ -5877,7 +6103,7 @@ export function createChecker(program: Program): Checker { return [Related.true, []]; } - if (!isValueType(source)) { + if (!isValue(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } @@ -5890,7 +6116,7 @@ export function createChecker(program: Program): Checker { diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if (source.kind === "ParamConstraintUnion") { + if ("kind" in source && source.kind === "ParamConstraintUnion") { for (const option of source.options) { const [variantAssignable] = isAssignableToParameterConstraintUnion( option, @@ -5921,46 +6147,47 @@ export function createChecker(program: Program): Checker { diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if (isUnknownType(target)) return [Related.true, []]; - if (isNullType(source)) { - return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); - } - if (target.kind === "Union") { - for (const option of target.variants.values()) { - const [related] = isValueOfTypeInternal( - source, - option.type, - diagnosticTarget, - relationCache - ); - if (related) { - return [Related.true, []]; - } - } - } - - switch (source.kind) { - case "ObjectLiteral": - if (target.kind === "Model" && !isArrayModelType(program, target)) { - return isObjectLiteralOfModelType(source, target, diagnosticTarget, relationCache); - } - break; - case "TupleLiteral": - if (target.kind === "Model" && isArrayModelType(program, target)) { - return isTupleLiteralOfArrayType(source, target, diagnosticTarget, relationCache); - } else if (target.kind === "Tuple") { - return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); - } - break; - case "String": - case "StringTemplate": - case "Number": - case "Boolean": - case "EnumMember": - return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); - } - - return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + return isTypeAssignableToInternal(source.type, target, diagnosticTarget, relationCache); + // TODO: cleanup + // if (isUnknownType(target)) return [Related.true, []]; + // if (source.valueKind === "NullValue") { + // return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); + // } + // if (target.kind === "Union") { + // for (const option of target.variants.values()) { + // const [related] = isValueOfTypeInternal( + // source, + // option.type, + // diagnosticTarget, + // relationCache + // ); + // if (related) { + // return [Related.true, []]; + // } + // } + // } + + // switch (source.valueKind) { + // case "ObjectValue": + // if (target.kind === "Model" && !isArrayModelType(program, target)) { + // return isObjectLiteralOfModelType(source, target, diagnosticTarget, relationCache); + // } + // break; + // case "ArrayValue": + // if (target.kind === "Model" && isArrayModelType(program, target)) { + // return isTupleLiteralOfArrayType(source, target, diagnosticTarget, relationCache); + // } else if (target.kind === "Tuple") { + // return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); + // } + // break; + // case "StringValue": + // case "NumericValue": + // case "BooleanValue": + // case "EnumMemberValue": + // return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); + // } + + // return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } function isReflectionType(type: Type): type is Model & { name: ReflectionTypeName } { @@ -6094,7 +6321,7 @@ export function createChecker(program: Program): Checker { } function isObjectLiteralOfModelType( - source: ObjectLiteral, + source: ObjectValue, target: Model, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> @@ -6121,7 +6348,7 @@ export function createChecker(program: Program): Checker { } else { remainingProperties.delete(prop.name); const [related, propDiagnostics] = isValueOfTypeInternal( - sourceProperty, + sourceProperty.value, prop.type, diagnosticTarget, relationCache @@ -6134,7 +6361,7 @@ export function createChecker(program: Program): Checker { if (target.indexer) { const [_, indexerDiagnostics] = arePropertiesAssignableToIndexer( - remainingProperties, + remainingProperties as any, // TODO: fix target.indexer.value, diagnosticTarget, relationCache @@ -6157,7 +6384,7 @@ export function createChecker(program: Program): Checker { return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; } function getObjectLiteralPropertyNode( - object: ObjectLiteral, + object: ObjectValue, propertyName: string ): DiagnosticTarget { return ( @@ -6168,7 +6395,7 @@ export function createChecker(program: Program): Checker { } function isTupleLiteralOfArrayType( - source: TupleLiteral, + source: ArrayValue, target: ArrayModelType, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> @@ -6288,7 +6515,7 @@ export function createChecker(program: Program): Checker { } function isTupleAssignableToTuple( - source: Tuple | TupleLiteral, + source: Tuple | ArrayValue, target: Tuple, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> @@ -6795,7 +7022,7 @@ function applyDecoratorToType(program: Program, decApp: DecoratorApplication, ta compilerAssert("decorators" in target, "Cannot apply decorator to non-decoratable type", target); for (const arg of decApp.args) { - if (isErrorType(arg.value)) { + if (isType(arg.value) && isErrorType(arg.value)) { // If one of the decorator argument is an error don't run it. return; } @@ -6885,6 +7112,7 @@ const ReflectionNameToKind = { const _assertReflectionNameToKind: Record = ReflectionNameToKind; enum ResolutionKind { + Value, Type, BaseType, Constraint, diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index e2c2ab91d8..98657cd2ce 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -83,7 +83,7 @@ export function getSourceLocation( return target; } - if (!("kind" in target)) { + if (!("kind" in target) && !("valueKind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { target = target.symbolSource!; @@ -94,7 +94,7 @@ export function getSourceLocation( } return getSourceLocationOfNode(target.declarations[0], options); - } else if (typeof target.kind === "number") { + } else if ("kind" in target && typeof target.kind === "number") { // node return getSourceLocationOfNode(target as Node, options); } else { diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index b1f411a2f0..184c6a8210 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -1,6 +1,6 @@ import { printId } from "../../formatter/print/printer.js"; -import { isTemplateInstance } from "../type-utils.js"; -import { +import { isTemplateInstance, isValue } from "../type-utils.js"; +import type { Entity, Enum, Interface, @@ -12,6 +12,7 @@ import { Scalar, Type, Union, + Value, } from "../types.js"; export interface TypeNameOptions { @@ -20,10 +21,6 @@ export interface TypeNameOptions { } export function getTypeName(type: Type, options?: TypeNameOptions): string { - return getEntityName(type, options); -} - -export function getEntityName(type: Entity, options?: TypeNameOptions): string { switch (type.kind) { case "Namespace": return getNamespaceFullName(type, options); @@ -58,17 +55,45 @@ export function getEntityName(type: Entity, options?: TypeNameOptions): string { return type.value.toString(); case "Intrinsic": return type.name; - case "Value": - return `valueof ${getTypeName(type.target, options)}`; - case "ParamConstraintUnion": - return type.options.map((x) => getEntityName(x, options)).join(" | "); - case "ObjectLiteral": - return `#{${[...type.properties.entries()].map(([name, value]) => `${name}: ${getEntityName(value, options)}`).join(", ")}}`; - case "TupleLiteral": - return `#[${type.values.map((x) => getEntityName(x, options)).join(", ")}]`; + default: + return `(unnamed type)`; } +} - return `(unnamed type)`; +function getValuePreview(value: Value, options?: TypeNameOptions): string { + switch (value.valueKind) { + case "ObjectValue": + return `#{${[...value.properties.entries()].map(([name, value]) => `${name}: ${getValuePreview(value.value, options)}`).join(", ")}}`; + case "ArrayValue": + return `#[${value.values.map((x) => getValuePreview(x, options)).join(", ")}]`; + case "StringValue": + return `"${value.value}"`; + case "BooleanValue": + return `${value.value}`; + case "NumericValue": + return `${value.value.toString()}`; + case "EnumMemberValue": + return getTypeName(value.value); + case "NullValue": + return "null"; + case "ScalarValue": + return `${getTypeName(value.type, options)}.${value.value.name}(${value.value.args.map((x) => getValuePreview(x, options)).join(", ")}})`; + } +} + +export function getEntityName(entity: Entity, options?: TypeNameOptions): string { + if (isValue(entity)) { + return getValuePreview(entity, options); + } else { + switch (entity.kind) { + case "Value": + return `valueof ${getTypeName(entity.target, options)}`; + case "ParamConstraintUnion": + return entity.options.map((x) => getEntityName(x, options)).join(" | "); + default: + return getTypeName(entity, options); + } + } } export function isStdNamespace(namespace: Namespace): boolean { diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 5cc88678bd..4f5800cef7 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,20 +1,20 @@ -import { isValueType, stringTemplateToString, typespecTypeToJson } from "./index.js"; +import { isValue, typespecTypeToJson } from "./index.js"; import type { - BooleanLiteral, + ArrayValue, + BooleanValue, Diagnostic, MarshalledValue, Model, - NumericLiteral, - ObjectLiteral, - StringLiteral, + NumericValue, + ObjectValue, + StringValue, Tuple, - TupleLiteral, Type, Value, } from "./types.js"; export function tryMarshallTypeForJS(type: T): MarshalledValue { - if (isValueType(type)) { + if (isValue(type)) { return marshallTypeForJS(type); } return type as any; @@ -24,44 +24,44 @@ export function tryMarshallTypeForJS(type: T): Marshalle export function marshallTypeForJSWithLegacyCast( type: T ): [MarshalledValue | undefined, readonly Diagnostic[]] { - switch (type.kind) { - case "Model": - case "Tuple": - return typespecTypeToJson(type, type) as any; - default: - return [marshallTypeForJS(type) as any, []]; + if ("kind" in type) { + return typespecTypeToJson(type, type) as any; + } else { + return [marshallTypeForJS(type) as any, []]; } } export function marshallTypeForJS(type: T): MarshalledValue { - switch (type.kind) { - case "Boolean": - case "String": - case "Number": - return literalTypeToValue(type) as any; - case "StringTemplate": - return stringTemplateToString(type)[0] as any; - case "ObjectLiteral": - return objectLiteralToValue(type) as any; - case "TupleLiteral": - return tupleLiteralToValue(type) as any; - case "EnumMember": + switch (type.valueKind) { + case "BooleanValue": + case "StringValue": + case "NumericValue": + return primitiveValueToJs(type) as any; + case "ObjectValue": + return objectValueToJs(type) as any; + case "ArrayValue": + return arrayValueToJs(type) as any; + case "EnumMemberValue": + return type.value as any; + case "NullValue": + return null as any; + case "ScalarValue": return type as any; } } -function literalTypeToValue( +function primitiveValueToJs( type: T ): MarshalledValue { return type.value as any; } -function objectLiteralToValue(type: ObjectLiteral) { +function objectValueToJs(type: ObjectValue) { const result: Record = {}; for (const [key, value] of type.properties) { - result[key] = marshallTypeForJS(value); + result[key] = marshallTypeForJS(value.value); } return result; } -function tupleLiteralToValue(type: TupleLiteral) { +function arrayValueToJs(type: ArrayValue) { return type.values.map(marshallTypeForJS); } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 732f0a241f..0170e9314f 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -903,6 +903,12 @@ const diagnostics = { default: paramMessage`Alias type '${"typeName"}' recursively references itself.`, }, }, + "circular-const": { + severity: "error", + messages: { + default: paramMessage`const '${"name"}' recursively references itself.`, + }, + }, "circular-prop": { severity: "error", messages: { diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 740e3db9d5..a7dcd7bf3e 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -1164,14 +1164,14 @@ export async function compile( } function getNode(target: Node | Entity | Sym): Node | undefined { - if (!("kind" in target)) { + if (!("kind" in target) && !("valueKind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { return target.symbolSource!.declarations[0]; } return target.declarations[0]; // handle multiple decls - } else if (typeof target.kind === "number") { + } else if ("kind" in target && typeof target.kind === "number") { // node return target as Node; } else { diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index 70a007adaa..c96f234914 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -2,12 +2,7 @@ import { createRekeyableMap, mutate } from "../utils/misc.js"; import { finishTypeForProgram } from "./checker.js"; import { compilerAssert } from "./diagnostics.js"; import { Program, ProjectedProgram, createStateAccessors, isProjectedProgram } from "./program.js"; -import { - getParentTemplateNode, - isNeverType, - isTemplateInstance, - isValueOnly, -} from "./type-utils.js"; +import { getParentTemplateNode, isNeverType, isTemplateInstance, isValue } from "./type-utils.js"; import { DecoratorApplication, DecoratorArgument, @@ -27,7 +22,6 @@ import { Union, UnionVariant, Value, - ValueOnly, } from "./types.js"; /** @@ -102,10 +96,10 @@ export function createProjector( return projectedProgram; function projectType(type: Type): Type; - function projectType(type: ValueOnly): ValueOnly; + function projectType(type: Value): Value; function projectType(type: Type | Value): Type | Value; function projectType(type: Type | Value): Type | Value { - if (isValueOnly(type)) { + if (isValue(type)) { return type; } if (projectedTypes.has(type)) { diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index 5626de5161..c71344ed0a 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -1,4 +1,3 @@ -import { isStringTemplateSerializable } from "./helpers/string-template-utils.js"; import { Program } from "./program.js"; import { Entity, @@ -20,59 +19,34 @@ import { TypeMapper, UnknownType, Value, - ValueOnly, VoidType, } from "./types.js"; -export function isErrorType(type: Entity): type is ErrorType { +export function isErrorType(type: Type): type is ErrorType { return type.kind === "Intrinsic" && type.name === "ErrorType"; } -export function isVoidType(type: Entity): type is VoidType { +export function isVoidType(type: Type): type is VoidType { return type.kind === "Intrinsic" && type.name === "void"; } -export function isNeverType(type: Entity): type is NeverType { +export function isNeverType(type: Type): type is NeverType { return type.kind === "Intrinsic" && type.name === "never"; } -export function isUnknownType(type: Entity): type is UnknownType { +export function isUnknownType(type: Type): type is UnknownType { return type.kind === "Intrinsic" && type.name === "unknown"; } -export function isNullType(type: Entity): type is NullType { +export function isNullType(type: Type): type is NullType { return type.kind === "Intrinsic" && type.name === "null"; } -const valueTypes = new Set([ - "String", - "Number", - "Boolean", - "EnumMember", - "ObjectLiteral", - "TupleLiteral", -]); - -export function isValueType(type: Entity): type is Value { - if (isNullType(type as any)) { - return true; - } - if (type.kind === "StringTemplate") { - const [valid] = isStringTemplateSerializable(type); - return valid; - } - - return valueTypes.has(type.kind); +export function isType(entity: Entity): entity is Type { + return "kind" in entity; } - -export function isValueOnly(type: Entity): type is ValueOnly { - switch (type.kind) { - case "ObjectLiteral": - case "TupleLiteral": - return true; - default: - return false; - } +export function isValue(entity: Entity): entity is Value { + return "valueKind" in entity; } /** diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 2cab0b7483..de21c10590 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1,19 +1,23 @@ import type { JSONSchemaType as AjvJSONSchemaType } from "ajv"; -import { TypeEmitter } from "../emitter-framework/type-emitter.js"; -import { AssetEmitter } from "../emitter-framework/types.js"; -import { YamlPathTarget, YamlScript } from "../yaml/types.js"; -import { ModuleResolutionResult } from "./module-resolver.js"; -import { Program } from "./program.js"; +import type { TypeEmitter } from "../emitter-framework/type-emitter.js"; +import type { AssetEmitter } from "../emitter-framework/types.js"; +import type { YamlPathTarget, YamlScript } from "../yaml/types.js"; +import type { ModuleResolutionResult } from "./module-resolver.js"; +import type { Numeric } from "./numeric.js"; +import type { Program } from "./program.js"; import type { TokenFlags } from "./scanner.js"; // prettier-ignore -export type MarshalledValue = - Type extends StringLiteral ? string - : Type extends NumericLiteral ? number - : Type extends BooleanLiteral ? boolean - : Type extends ObjectLiteral ? Record - : Type extends TupleLiteral ? unknown[] - : Type +export type MarshalledValue = +Value extends StringValue ? string + : Value extends NumericValue ? number + : Value extends BooleanValue ? boolean + : Value extends ObjectValue ? Record + : Value extends ArrayValue ? unknown[] + : Value extends EnumMemberValue ? EnumMember + : Value extends NullValue ? null + : Value extends ScalarValue ? Value + : Value /** * Type System types @@ -96,47 +100,30 @@ export interface TemplatedTypeBase { */ export type Entity = Type | Value | ValueType | ParamConstraintUnion; -/** Entities that can be used as both values and values. */ -export type TypeAndValue = - | StringLiteral - | StringTemplate - | NumericLiteral +export type Type = | BooleanLiteral - | EnumMember; - -/** - * Entities that can only be used as value. - */ -export type ValueOnly = ObjectLiteral | TupleLiteral; - -/** Entities that can be used as types only */ -export type TypeOnly = + | Decorator + | Enum + | EnumMember + | FunctionParameter + | FunctionType + | Interface + | IntrinsicType | Model | ModelProperty - | Scalar - | Interface - | Enum - | TemplateParameter | Namespace + | NumericLiteral + | ObjectType | Operation + | Projection + | Scalar + | StringLiteral + | StringTemplate | StringTemplateSpan + | TemplateParameter | Tuple | Union - | UnionVariant - | IntrinsicType - | FunctionType - | Decorator - | FunctionParameter - | ObjectType - | Projection; - -/** Entities that can be used as types */ -export type Type = TypeAndValue | TypeOnly; - -/** - * Entities that can be used as values. - */ -export type Value = TypeAndValue | ValueOnly; + | UnionVariant; export type StdTypes = { // Models @@ -312,22 +299,75 @@ export interface ModelProperty extends BaseType, DecoratedType { // this tracks the property we copied from. sourceProperty?: ModelProperty; optional: boolean; - default?: Type | Value; + default?: Type; + defaultValue?: Value; model?: Model; } -export interface ObjectLiteral { - kind: "ObjectLiteral"; +//#region Values +export type Value = + | ScalarValue + | NumericValue + | StringValue + | BooleanValue + | ObjectValue + | ArrayValue + | EnumMemberValue + | NullValue; + +interface BaseValue { + valueKind: string; + type: Type; // Every value has a type. That type could be something completely different(much wider type) +} + +export interface ObjectValue extends BaseValue { + valueKind: "ObjectValue"; node: ObjectLiteralNode; - properties: Map; + properties: Map; } -export interface TupleLiteral { - kind: "TupleLiteral"; +export interface ObjectValuePropertyDescriptor { + name: string; + value: Value; +} + +export interface ArrayValue extends BaseValue { + valueKind: "ArrayValue"; node: TupleLiteralNode; values: Value[]; } +export interface ScalarValue extends BaseValue { + valueKind: "ScalarValue"; + scalar: Scalar; // We need to keep a reference of what scalar this is. + value: { name: string; args: Value[] }; // e.g. for utcDateTime(2020,12,01) +} +export interface NumericValue extends BaseValue { + valueKind: "NumericValue"; + scalar: Scalar | undefined; + value: Numeric; +} +export interface StringValue extends BaseValue { + valueKind: "StringValue"; + scalar: Scalar | undefined; + value: string; +} +export interface BooleanValue extends BaseValue { + valueKind: "BooleanValue"; + scalar: Scalar | undefined; + value: boolean; +} +export interface EnumMemberValue extends BaseValue { + valueKind: "EnumMemberValue"; + value: EnumMember; +} +export interface NullValue extends BaseValue { + valueKind: "NullValue"; + value: null; +} + +//#endregion Values + export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Scalar"; name: string; @@ -728,12 +768,13 @@ export const enum SymbolFlags { SourceFile = 1 << 21, Declaration = 1 << 22, Implementation = 1 << 23, + Const = 1 << 24, /** * A symbol which was late-bound, in which case, the type referred to * by this symbol is stored directly in the symbol. */ - LateBound = 1 << 24, + LateBound = 1 << 25, ExportContainer = Namespace | SourceFile, /** @@ -1075,6 +1116,7 @@ export type Declaration = | ProjectionLambdaParameterDeclarationNode | EnumStatementNode | AliasStatementNode + | ConstStatementNode | DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode; diff --git a/packages/compiler/src/emitter-framework/type-emitter.ts b/packages/compiler/src/emitter-framework/type-emitter.ts index ef2bee27cf..5fc29a780f 100644 --- a/packages/compiler/src/emitter-framework/type-emitter.ts +++ b/packages/compiler/src/emitter-framework/type-emitter.ts @@ -780,6 +780,9 @@ export class TypeEmitter> { let unspeakable = false; const parameterNames = declarationType.templateMapper.args.map((t) => { + if (!("kind" in t)) { + return undefined; + } switch (t.kind) { case "Model": case "Scalar": diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 6ddbf14dec..886e7ebbba 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -133,7 +133,7 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - deepStrictEqual(foo.default?.kind, "TupleLiteral"); + deepStrictEqual(foo.defaultValue?.valueKind, "ArrayValue"); }); it(`foo?: {name: string} = #{name: "abc"}`, async () => { @@ -144,7 +144,7 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - deepStrictEqual(foo.default?.kind, "ObjectLiteral"); + deepStrictEqual(foo.defaultValue?.valueKind, "ObjectValue"); }); }); @@ -481,8 +481,9 @@ describe("compiler: models", () => { strictEqual(Pet.derivedModels[1].name, "TPet"); ok(Pet.derivedModels[1].templateMapper?.args); - strictEqual(Pet.derivedModels[1].templateMapper?.args[0].kind, "Scalar"); - strictEqual(Pet.derivedModels[1].templateMapper?.args[0].name, "string"); + ok("kind" in Pet.derivedModels[1].templateMapper!.args[0]); + strictEqual(Pet.derivedModels[1].templateMapper.args[0].kind, "Scalar"); + strictEqual(Pet.derivedModels[1].templateMapper.args[0].name, "string"); strictEqual(Pet.derivedModels[2], Cat); strictEqual(Pet.derivedModels[3], Dog); diff --git a/packages/compiler/test/checker/scalar.test.ts b/packages/compiler/test/checker/scalar.test.ts index 7c03b88799..cc707a7e49 100644 --- a/packages/compiler/test/checker/scalar.test.ts +++ b/packages/compiler/test/checker/scalar.test.ts @@ -1,6 +1,6 @@ import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Model, NumericLiteral } from "../../src/core/index.js"; +import { Model } from "../../src/core/index.js"; import { BasicTestRunner, createTestHost, @@ -22,7 +22,7 @@ describe("compiler: scalars", () => { @test scalar A; `); - strictEqual(A.kind, "Scalar" as const); + strictEqual(A.kind, "Scalar"); strictEqual(A.name, "A"); strictEqual(A.baseScalar, undefined); }); @@ -32,7 +32,7 @@ describe("compiler: scalars", () => { @test scalar A extends numeric; `); - strictEqual(A.kind, "Scalar" as const); + strictEqual(A.kind, "Scalar"); strictEqual(A.name, "A"); strictEqual(A.baseScalar, runner.program.checker.getStdType("numeric")); }); @@ -46,7 +46,7 @@ describe("compiler: scalars", () => { alias B = A<"123">; `); - strictEqual(A.kind, "Scalar" as const); + strictEqual(A.kind, "Scalar"); strictEqual(A.name, "A"); }); @@ -60,8 +60,8 @@ describe("compiler: scalars", () => { alias BIns = B<"">; `); - strictEqual(A.kind, "Scalar" as const); - strictEqual(B.kind, "Scalar" as const); + strictEqual(A.kind, "Scalar"); + strictEqual(B.kind, "Scalar"); }); it("allows a decimal to have a default value", async () => { @@ -71,10 +71,9 @@ describe("compiler: scalars", () => { } `)) as { A: Model }; - const def = A.properties.get("x")!.default! as NumericLiteral; - strictEqual(def.kind, "Number" as const); - strictEqual(def.value, 42); - strictEqual(def.valueAsString, "42"); + const def = A.properties.get("x")!.defaultValue!; + strictEqual(def.valueKind, "NumericValue"); + strictEqual(def.value.asNumber(), 42); }); describe("custom scalars and default values", () => { @@ -84,13 +83,13 @@ describe("compiler: scalars", () => { @test model M { p?: S = 42; } `); - strictEqual(S.kind, "Scalar" as const); - strictEqual(M.kind, "Model" as const); + strictEqual(S.kind, "Scalar"); + strictEqual(M.kind, "Model"); const p = M.properties.get("p"); ok(p); expectIdenticalTypes(p.type, S); - strictEqual(p.default?.kind, "Number" as const); - strictEqual(p.default.value, 42); + strictEqual(p.defaultValue?.valueKind, "NumericValue"); + strictEqual(p.defaultValue.value, 42); }); it("allows custom boolean scalar to have a default value", async () => { @@ -99,13 +98,13 @@ describe("compiler: scalars", () => { @test model M { p?: S = true; } `); - strictEqual(S.kind, "Scalar" as const); - strictEqual(M.kind, "Model" as const); + strictEqual(S.kind, "Scalar"); + strictEqual(M.kind, "Model"); const p = M.properties.get("p"); ok(p); expectIdenticalTypes(p.type, S); - strictEqual(p.default?.kind, "Boolean" as const); - strictEqual(p.default.value, true); + strictEqual(p.defaultValue?.valueKind, "BooleanValue"); + strictEqual(p.defaultValue.value, true); }); it("allows custom string scalar to have a default value", async () => { @@ -114,13 +113,13 @@ describe("compiler: scalars", () => { @test model M { p?: S = "hello"; } `); - strictEqual(S.kind, "Scalar" as const); - strictEqual(M.kind, "Model" as const); + strictEqual(S.kind, "Scalar"); + strictEqual(M.kind, "Model"); const p = M.properties.get("p"); ok(p); expectIdenticalTypes(p.type, S); - strictEqual(p.default?.kind, "String" as const); - strictEqual(p.default.value, "hello"); + strictEqual(p.defaultValue?.valueKind, "StringValue"); + strictEqual(p.defaultValue.value, "hello"); }); it("does not allow custom numeric scalar to have a default outside range", async () => { diff --git a/packages/compiler/test/checker/values.test.ts b/packages/compiler/test/checker/values.test.ts index 8067f6b3bf..aaff87fbc8 100644 --- a/packages/compiler/test/checker/values.test.ts +++ b/packages/compiler/test/checker/values.test.ts @@ -60,31 +60,31 @@ async function diagnoseValueType(code: string, other?: string): Promise { it("no properties", async () => { const object = await compileValueType(`#{}`); - strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 0); }); it("single property", async () => { const object = await compileValueType(`#{name: "John"}`); - strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 1); - const nameProp = object.properties.get("name"); - strictEqual(nameProp?.kind, "String"); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); strictEqual(nameProp.value, "John"); }); it("multiple property", async () => { const object = await compileValueType(`#{name: "John", age: 21}`); - strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 2); - const nameProp = object.properties.get("name"); - strictEqual(nameProp?.kind, "String"); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); strictEqual(nameProp.value, "John"); - const ageProp = object.properties.get("age"); - strictEqual(ageProp?.kind, "Number"); - strictEqual(ageProp.value, 21); + const ageProp = object.properties.get("age")?.value; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value.asNumber(), 21); }); describe("spreading", () => { @@ -93,16 +93,16 @@ describe("object literals", () => { `#{...Common, age: 21}`, `alias Common = #{ name: "John" };` ); - strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 2); - const nameProp = object.properties.get("name"); - strictEqual(nameProp?.kind, "String"); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); strictEqual(nameProp.value, "John"); - const ageProp = object.properties.get("age"); - strictEqual(ageProp?.kind, "Number"); - strictEqual(ageProp.value, 21); + const ageProp = object.properties.get("age")?.value; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value.asNumber(), 21); }); it("override properties defined before if there is a name conflict", async () => { @@ -110,10 +110,10 @@ describe("object literals", () => { `#{name: "John", age: 21, ...Common, }`, `alias Common = #{ name: "Common" };` ); - strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.valueKind, "ObjectValue"); - const nameProp = object.properties.get("name"); - strictEqual(nameProp?.kind, "String"); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); strictEqual(nameProp.value, "Common"); }); @@ -122,10 +122,10 @@ describe("object literals", () => { `#{...Common, name: "John", age: 21 }`, `alias Common = #{ name: "John" };` ); - strictEqual(object.kind, "ObjectLiteral"); + strictEqual(object.valueKind, "ObjectValue"); - const nameProp = object.properties.get("name"); - strictEqual(nameProp?.kind, "String"); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); strictEqual(nameProp.value, "John"); }); @@ -143,17 +143,17 @@ describe("object literals", () => { describe("valid property types", () => { it.each([ - ["String", `"John"`], - ["Number", "21"], + ["StringValue", `"John"`], + ["NumericValue", "21"], ["Boolean", "true"], ["EnumMember", "Direction.up", "enum Direction { up, down }"], - ["ObjectLiteral", `#{nested: "foo"}`], - ["TupleLiteral", `#["foo"]`], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], ])("%s", async (kind, type, other?) => { const object = await compileValueType(`#{prop: ${type}}`, other); - strictEqual(object.kind, "ObjectLiteral"); - const nameProp = object.properties.get("prop"); - strictEqual(nameProp?.kind, kind); + strictEqual(object.valueKind, "ObjectValue"); + const nameProp = object.properties.get("prop")?.value; + strictEqual(nameProp?.valueKind, kind); }); }); @@ -195,46 +195,46 @@ describe("object literals", () => { describe("tuple literals", () => { it("no values", async () => { const object = await compileValueType(`#[]`); - strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.valueKind, "ArrayValue"); strictEqual(object.values.length, 0); }); it("single value", async () => { const object = await compileValueType(`#["John"]`); - strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.valueKind, "ArrayValue"); strictEqual(object.values.length, 1); const first = object.values[0]; - strictEqual(first.kind, "String"); + strictEqual(first.valueKind, "StringValue"); strictEqual(first.value, "John"); }); it("multiple property", async () => { const object = await compileValueType(`#["John", 21]`); - strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.valueKind, "ArrayValue"); strictEqual(object.values.length, 2); const nameProp = object.values[0]; - strictEqual(nameProp?.kind, "String"); + strictEqual(nameProp?.valueKind, "StringValue"); strictEqual(nameProp.value, "John"); const ageProp = object.values[1]; - strictEqual(ageProp?.kind, "Number"); + strictEqual(ageProp?.valueKind, "NumericValue"); strictEqual(ageProp.value, 21); }); describe("valid property types", () => { it.each([ - ["String", `"John"`], - ["Number", "21"], + ["StringValue", `"John"`], + ["NumericValue", "21"], ["Boolean", "true"], ["EnumMember", "Direction.up", "enum Direction { up, down }"], - ["ObjectLiteral", `#{nested: "foo"}`], - ["TupleLiteral", `#["foo"]`], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], ])("%s", async (kind, type, other?) => { const object = await compileValueType(`#[${type}]`, other); - strictEqual(object.kind, "TupleLiteral"); + strictEqual(object.valueKind, "ArrayValue"); const nameProp = object.values[0]; - strictEqual(nameProp?.kind, kind); + strictEqual(nameProp?.valueKind, kind); }); }); diff --git a/packages/compiler/test/projection/projector-identity.test.ts b/packages/compiler/test/projection/projector-identity.test.ts index 05962b5cc7..1123c3084b 100644 --- a/packages/compiler/test/projection/projector-identity.test.ts +++ b/packages/compiler/test/projection/projector-identity.test.ts @@ -1,12 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { - DecoratorContext, - Namespace, - Type, - getTypeName, - isValueOnly, -} from "../../src/core/index.js"; +import { DecoratorContext, Namespace, Type, getTypeName, isValue } from "../../src/core/index.js"; import { createProjector } from "../../src/core/projector.js"; import { createTestHost, createTestRunner } from "../../src/testing/test-host.js"; import { BasicTestRunner, TestHost } from "../../src/testing/types.js"; @@ -382,22 +376,22 @@ describe("compiler: projector: Identity", () => { ok(value !== original.templateMapper.map.get(key)); } for (const arg of original.templateMapper.args) { - if (!isValueOnly(arg)) { + if (!isValue(arg)) { ok(arg.projector === original.projector); } } for (const value of original.templateMapper.map.values()) { - if (!isValueOnly(value)) { + if (!isValue(value)) { ok(value.projector === original.projector); } } for (const arg of projected.templateMapper.args) { - if (!isValueOnly(arg)) { + if (!isValue(arg)) { ok(arg.projector === projected.projector); } } for (const value of projected.templateMapper.map.values()) { - if (!isValueOnly(value)) { + if (!isValue(value)) { ok(value.projector === projected.projector); } } diff --git a/packages/protobuf/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index d5ad05278f..0b42e70744 100644 --- a/packages/protobuf/src/transform/index.ts +++ b/packages/protobuf/src/transform/index.ts @@ -14,7 +14,7 @@ import { IntrinsicType, isDeclaredInNamespace, isTemplateInstance, - isValueOnly, + isValue, Model, ModelProperty, Namespace, @@ -538,8 +538,8 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P function mapToProto(t: Model, relativeSource: Model | Operation): ProtoMap { const [keyType, valueType] = t.templateMapper!.args; - compilerAssert(!isValueOnly(keyType), "Cannot be a value type"); - compilerAssert(!isValueOnly(valueType), "Cannot be a value type"); + compilerAssert(!isValue(keyType), "Cannot be a value type"); + compilerAssert(!isValue(valueType), "Cannot be a value type"); // A map's value cannot be another map. if (isMap(program, keyType)) { reportDiagnostic(program, { @@ -562,7 +562,7 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P function arrayToProto(t: Model, relativeSource: Model | Operation): ProtoType { const valueType = (t as Model).templateMapper!.args[0]; - compilerAssert(!isValueOnly(valueType), "Cannot be a value type"); + compilerAssert(!isValue(valueType), "Cannot be a value type"); // Nested arrays are not supported. if (isArray(valueType)) { diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts index b8c3a201c2..25ca7a48b7 100644 --- a/packages/tspd/src/ref-doc/emitters/markdown.ts +++ b/packages/tspd/src/ref-doc/emitters/markdown.ts @@ -1,4 +1,4 @@ -import { Entity, getEntityName, isValueOnly, resolvePath } from "@typespec/compiler"; +import { Entity, getEntityName, isValue, resolvePath } from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -191,7 +191,7 @@ export class MarkdownRenderer { const namedType = type.kind !== "Value" && type.kind !== "ParamConstraintUnion" && - !isValueOnly(type) && + !isValue(type) && this.refDoc.getNamedTypeRefDoc(type); if (namedType) { return link( diff --git a/packages/versioning/src/validate.ts b/packages/versioning/src/validate.ts index ada20803d1..fc2e700279 100644 --- a/packages/versioning/src/validate.ts +++ b/packages/versioning/src/validate.ts @@ -3,7 +3,7 @@ import { getService, getTypeName, isTemplateInstance, - isValueOnly, + isValue, Namespace, navigateProgram, NoTarget, @@ -474,7 +474,7 @@ function validateReference(program: Program, source: Type, target: Type) { if ("templateMapper" in target) { for (const param of target.templateMapper?.args ?? []) { - if (!isValueOnly(param)) { + if (!isValue(param)) { validateTargetVersionCompatible(program, source, param); } } From 3da5cff48d749c1645af641102d838bda32d3a15 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 11:03:19 -0700 Subject: [PATCH 044/184] call expression --- packages/compiler/src/core/parser.ts | 34 ++++++++++++++++++- packages/compiler/src/core/types.ts | 8 +++++ .../compiler/src/formatter/print/printer.ts | 18 ++++++++-- packages/spec/src/spec.emu.html | 14 ++++++-- 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 6e7b1aa27e..a580d9e154 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -19,6 +19,7 @@ import { AugmentDecoratorStatementNode, BlockComment, BooleanLiteralNode, + CallExpressionNode, Comment, ConstStatementNode, DeclarationNode, @@ -190,6 +191,11 @@ namespace ListKind { invalidAnnotationTarget: "expression", } as const; + export const FunctionArguments = { + ...OperationParameters, + invalidAnnotationTarget: "expression", + } as const; + export const ModelProperties = { ...PropertiesBase, open: Token.OpenBrace, @@ -1251,6 +1257,30 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): TypeReferenceNode { const pos = tokenPos(); const target = parseIdentifierOrMemberExpression(message); + return parseReferenceExpressionInternal(target, pos); + } + + function parseCallOrReferenceExpression( + message?: keyof CompilerDiagnostics["token-expected"] + ): TypeReferenceNode | CallExpressionNode { + const pos = tokenPos(); + const target = parseIdentifierOrMemberExpression(message); + if (parseOptional(Token.OpenParen)) { + return { + kind: SyntaxKind.CallExpression, + target, + arguments: parseList(ListKind.FunctionArguments, parseExpression), + ...finishNode(pos), + }; + } + + return parseReferenceExpressionInternal(target, pos); + } + + function parseReferenceExpressionInternal( + target: IdentifierNode | MemberExpressionNode, + pos: number + ): TypeReferenceNode { const args = parseOptionalList(ListKind.TemplateArguments, parseTemplateArgument); return { @@ -1485,7 +1515,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.ValueOfKeyword: return parseValueOfExpression(); case Token.Identifier: - return parseReferenceExpression(); + return parseCallOrReferenceExpression(); case Token.StringLiteral: return parseStringLiteral(); case Token.StringTemplateHead: @@ -3303,6 +3333,8 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined ); case SyntaxKind.DecoratorExpression: return visitNode(cb, node.target) || visitEach(cb, node.arguments); + case SyntaxKind.CallExpression: + return visitNode(cb, node.target) || visitEach(cb, node.arguments); case SyntaxKind.DirectiveExpression: return visitNode(cb, node.target) || visitEach(cb, node.arguments); case SyntaxKind.ImportStatement: diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index de21c10590..70e6517b4c 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -910,6 +910,7 @@ export enum SyntaxKind { ObjectLiteralSpreadProperty, TupleLiteral, ConstStatement, + CallExpression, } export const enum NodeFlags { @@ -1095,6 +1096,7 @@ export type Statement = | FunctionDeclarationStatementNode | AugmentDecoratorStatementNode | ConstStatementNode + | CallExpressionNode | EmptyStatementNode | InvalidStatementNode | ProjectionStatementNode; @@ -1174,6 +1176,7 @@ export type Expression = | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode + | CallExpressionNode | StringLiteralNode | NumericLiteralNode | BooleanLiteralNode @@ -1326,6 +1329,11 @@ export interface ConstStatementNode extends BaseNode, DeclarationNode { readonly type?: Expression; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } +export interface CallExpressionNode extends BaseNode { + readonly kind: SyntaxKind.CallExpression; + readonly target: MemberExpressionNode | IdentifierNode; + readonly arguments: Expression[]; +} export interface InvalidStatementNode extends BaseNode { readonly kind: SyntaxKind.InvalidStatement; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 3fd800a62f..9afa054594 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -9,6 +9,7 @@ import { AugmentDecoratorStatementNode, BlockComment, BooleanLiteralNode, + CallExpressionNode, Comment, ConstStatementNode, DecoratorDeclarationStatementNode, @@ -387,6 +388,8 @@ export function printNode( return printTupleLiteral(path as AstPath, options, print); case SyntaxKind.ConstStatement: return printConstStatement(path as AstPath, options, print); + case SyntaxKind.CallExpression: + return printCallExpression(path as AstPath, options, print); case SyntaxKind.StringTemplateSpan: case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: @@ -440,6 +443,15 @@ export function printConstStatement( return ["const ", id, type, " = ", path.call(print, "value"), ";"]; } +export function printCallExpression( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + const args = printCallOrDecoratorArgs(path, options, print); + return [path.call(print, "target"), args]; +} + function printTemplateParameters( path: AstPath, options: TypeSpecPrettierOptions, @@ -590,7 +602,7 @@ export function printDecorator( options: TypeSpecPrettierOptions, print: PrettierChildPrint ) { - const args = printDecoratorArgs(path, options, print); + const args = printCallOrDecoratorArgs(path, options, print); return ["@", path.call(print, "target"), args]; } @@ -653,8 +665,8 @@ export function printDirective( return ["#", path.call(print, "target"), " ", args]; } -function printDecoratorArgs( - path: AstPath, +function printCallOrDecoratorArgs( + path: AstPath, options: TypeSpecPrettierOptions, print: PrettierChildPrint ) { diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 4b6a60cf17..1fa5128644 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -476,7 +476,7 @@

Syntactic Grammar

PrimaryExpression : Literal - ReferenceExpression + CallOrReferenceExpression ParenthesizedExpression ObjectLiteral TupleLiteral @@ -488,7 +488,13 @@

Syntactic Grammar

BooleanLiteral NumericLiteral -ReferenceExpression : +CallOrReferenceExpression : + CallExpression + ReferenceExpression +CallExpression + IdentifierOrMemberExpression CallArguments + +ReferenceExpression IdentifierOrMemberExpression TemplateArguments? ReferenceExpressionList : @@ -547,6 +553,10 @@

Syntactic Grammar

DecoratorArguments : `(` ExpressionList? `)` +CallExpression : + IdentifierOrMemberExpression `(` ExpressionList? `)` + + AugmentDecoratorStatement : `@@` IdentifierOrMemberExpression DecoratorArguments? From 0126ffa4f6407f27f153a47bc8c3d2ae98628e23 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 11:42:00 -0700 Subject: [PATCH 045/184] add tests for call expression parsing --- packages/compiler/src/core/parser.ts | 2 +- packages/compiler/test/parser.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index a580d9e154..ba1cd7e6e9 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1265,7 +1265,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): TypeReferenceNode | CallExpressionNode { const pos = tokenPos(); const target = parseIdentifierOrMemberExpression(message); - if (parseOptional(Token.OpenParen)) { + if (token() === Token.OpenParen) { return { kind: SyntaxKind.CallExpression, target, diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 56eec40bf1..740dfa6e9e 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -237,6 +237,18 @@ describe("compiler: parser", () => { ]); }); + describe("call expressions", () => { + parseEach([ + `const a = int8(123);`, + `const a = utcDateTime.fromISO("abc");`, + `const a = utcDateTime.fromISO("abc", "def");`, + ]); + parseErrorEach([ + [`const a = int8(123;`, [{ message: "')' expected." }]], + [`const a = utcDateTime.fromISO(;`, [{ message: "Expression expected." }]], + ]); + }); + describe("object literals", () => { parseEach([ `const A = #{a: "abc"};`, From 134d3b679cc3deadb9f9f2f183003ac1ab5dcff8 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 13:28:36 -0700 Subject: [PATCH 046/184] Add scalar constructors parsing and formatter --- packages/compiler/src/core/parser.ts | 46 +++++++++++++ packages/compiler/src/core/scanner.ts | 3 + packages/compiler/src/core/types.ts | 11 +++ .../src/formatter/print/comment-handler.ts | 26 +++++++ .../compiler/src/formatter/print/printer.ts | 51 +++++++++++++- .../compiler/test/formatter/formatter.test.ts | 67 +++++++++++++++++++ packages/compiler/test/parser.test.ts | 11 +-- 7 files changed, 209 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index ba1cd7e6e9..adec961516 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -93,6 +93,7 @@ import { ProjectionTupleExpressionNode, ProjectionUnionSelectorNode, ProjectionUnionVariantSelectorNode, + ScalarConstructorNode, ScalarStatementNode, SourceFile, Statement, @@ -222,6 +223,16 @@ namespace ListKind { allowedStatementKeyword: Token.OpKeyword, } as const; + export const ScalarMembers = { + ...PropertiesBase, + open: Token.OpenBrace, + close: Token.CloseBrace, + delimiter: Token.Semicolon, + toleratedDelimiter: Token.Comma, + toleratedDelimiterIsValid: false, + allowedStatementKeyword: Token.InitKeyword, + } as const; + export const UnionVariants = { ...PropertiesBase, open: Token.OpenBrace, @@ -574,6 +585,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "alias statement"); item = parseAliasStatement(pos); break; + case Token.ConstKeyword: + reportInvalidDecorators(decorators, "const statement"); + item = parseConstStatement(pos); + break; case Token.UsingKeyword: reportInvalidDecorators(decorators, "using statement"); item = parseUsingStatement(pos); @@ -1040,12 +1055,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const templateParameters = parseTemplateParameterList(); const optionalExtends = parseOptionalScalarExtends(); + const members = parseScalarMembers(); return { kind: SyntaxKind.ScalarStatement, id, templateParameters, extends: optionalExtends, + members, decorators, ...finishNode(pos), }; @@ -1058,6 +1075,32 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return undefined; } + function parseScalarMembers(): readonly ScalarConstructorNode[] { + if (token() === Token.Semicolon) { + nextToken(); + return []; + } else { + return parseList(ListKind.ScalarMembers, parseScalarMember); + } + } + + function parseScalarMember( + pos: number, + decorators: DecoratorExpressionNode[] + ): ScalarConstructorNode { + reportInvalidDecorators(decorators, "spread property"); + + parseExpected(Token.InitKeyword); + const id = parseIdentifier(); + const parameters = parseFunctionParameters(); + return { + kind: SyntaxKind.ScalarConstructor, + id, + parameters, + ...finishNode(pos), + }; + } + function parseEnumStatement( pos: number, decorators: DecoratorExpressionNode[] @@ -3396,8 +3439,11 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitEach(cb, node.templateParameters) || + visitEach(cb, node.members) || visitNode(cb, node.extends) ); + case SyntaxKind.ScalarConstructor: + return visitNode(cb, node.id) || visitEach(cb, node.parameters); case SyntaxKind.UnionStatement: return ( visitEach(cb, node.decorators) || diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 21f6d7f2f7..97924f89d7 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -126,6 +126,7 @@ export enum Token { FnKeyword, ValueOfKeyword, ConstKeyword, + InitKeyword, // Add new statement keyword above /** @internal */ __EndStatementKeyword, @@ -249,6 +250,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.FnKeyword, "'fn'"], [Token.ValueOfKeyword, "'valueof'"], [Token.ConstKeyword, "'const'"], + [Token.InitKeyword, "'init'"], [Token.ExtendsKeyword, "'extends'"], [Token.TrueKeyword, "'true'"], [Token.FalseKeyword, "'false'"], @@ -280,6 +282,7 @@ export const Keywords: ReadonlyMap = new Map([ ["fn", Token.FnKeyword], ["valueof", Token.ValueOfKeyword], ["const", Token.ConstKeyword], + ["init", Token.InitKeyword], ["true", Token.TrueKeyword], ["false", Token.FalseKeyword], ["return", Token.ReturnKeyword], diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 70e6517b4c..996143e940 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -911,6 +911,7 @@ export enum SyntaxKind { TupleLiteral, ConstStatement, CallExpression, + ScalarConstructor, } export const enum NodeFlags { @@ -1010,6 +1011,7 @@ export type Node = | ObjectLiteralNode | ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode + | ScalarConstructorNode | TupleLiteralNode; /** @@ -1271,9 +1273,18 @@ export interface ScalarStatementNode extends BaseNode, DeclarationNode, Template readonly kind: SyntaxKind.ScalarStatement; readonly extends?: TypeReferenceNode; readonly decorators: readonly DecoratorExpressionNode[]; + readonly members: readonly ScalarConstructorNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } +// TODO: should this be ScalarConstructorDeclarationNode? +export interface ScalarConstructorNode extends BaseNode { + readonly kind: SyntaxKind.ScalarConstructor; + readonly id: IdentifierNode; + readonly parameters: FunctionParameterNode[]; + readonly parent?: ScalarStatementNode; +} + export interface InterfaceStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.InterfaceStatement; readonly operations: readonly OperationStatementNode[]; diff --git a/packages/compiler/src/formatter/print/comment-handler.ts b/packages/compiler/src/formatter/print/comment-handler.ts index 7d239f27de..589ac66b26 100644 --- a/packages/compiler/src/formatter/print/comment-handler.ts +++ b/packages/compiler/src/formatter/print/comment-handler.ts @@ -17,6 +17,7 @@ export const commentHandler: Printer["handleComments"] = { [ addEmptyInterfaceComment, addEmptyModelComment, + addEmptyScalarComment, addCommentBetweenAnnotationsAndNode, handleOnlyComments, ].some((x) => x({ comment, text, options, ast: ast as TypeSpecScriptNode, isLastComment })), @@ -125,6 +126,31 @@ function addEmptyModelComment({ comment }: CommentContext) { return false; } +/** + * When a comment is on an empty scalar make sure it gets added as a dangling comment on it and not on the identifier. + * + * @example + * + * scalar foo { + * // My comment + * } + */ +function addEmptyScalarComment({ comment }: CommentContext) { + const { precedingNode, enclosingNode } = comment; + + if ( + enclosingNode && + enclosingNode.kind === SyntaxKind.ScalarStatement && + enclosingNode.members.length === 0 && + precedingNode && + (precedingNode === enclosingNode.id || precedingNode === enclosingNode.extends) + ) { + util.addDanglingComment(enclosingNode, comment, undefined); + return true; + } + return false; +} + function handleOnlyComments({ comment, ast, isLastComment }: CommentContext) { const { enclosingNode } = comment; if (ast?.statements?.length === 0) { diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 9afa054594..0a233c0822 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -60,6 +60,7 @@ import { ProjectionTupleExpressionNode, ProjectionUnaryExpressionNode, ReturnExpressionNode, + ScalarConstructorNode, ScalarStatementNode, Statement, StringLiteralNode, @@ -172,6 +173,8 @@ export function printNode( return printModelStatement(path as AstPath, options, print); case SyntaxKind.ScalarStatement: return printScalarStatement(path as AstPath, options, print); + case SyntaxKind.ScalarConstructor: + return printScalarConstructor(path as AstPath, options, print); case SyntaxKind.AliasStatement: return printAliasStatement(path as AstPath, options, print); case SyntaxKind.EnumStatement: @@ -1177,6 +1180,7 @@ function shouldWrapMemberInNewLines( | ModelSpreadPropertyNode | EnumMemberNode | EnumSpreadMemberNode + | ScalarConstructorNode | UnionVariantNode | ProjectionModelPropertyNode | ProjectionModelSpreadPropertyNode @@ -1190,6 +1194,7 @@ function shouldWrapMemberInNewLines( (node.kind !== SyntaxKind.ModelSpreadProperty && node.kind !== SyntaxKind.ProjectionModelSpreadProperty && node.kind !== SyntaxKind.EnumSpreadMember && + node.kind !== SyntaxKind.ScalarConstructor && node.kind !== SyntaxKind.ObjectLiteralProperty && node.kind !== SyntaxKind.ObjectLiteralSpreadProperty && shouldDecoratorBreakLine(path as any, options, { @@ -1289,7 +1294,7 @@ function isModelExpressionInBlock( } } -export function printScalarStatement( +function printScalarStatement( path: AstPath, options: TypeSpecPrettierOptions, print: PrettierChildPrint @@ -1301,14 +1306,56 @@ export function printScalarStatement( const heritage = node.extends ? [ifBreak(line, " "), "extends ", path.call(print, "extends")] : ""; + const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling); + const shouldPrintBody = nodeHasComments || !(node.members.length === 0); + + const members = shouldPrintBody ? [" ", printScalarBody(path, options, print)] : ";"; return [ printDecorators(path, options, print, { tryInline: false }).decorators, "scalar ", id, template, group(indent(["", heritage])), - ";", + members, + ]; +} + +function printScalarBody( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + const node = path.node; + const hasProperties = node.members && node.members.length > 0; + const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling); + if (!hasProperties && !nodeHasComments) { + return "{}"; + } + const body = [joinMembersInBlock(path, "members", options, print, ";", hardline)]; + if (nodeHasComments) { + body.push(printDanglingComments(path, options, { sameIndent: true })); + } + return group(["{", indent(body), hardline, "}"]); +} + +function printScalarConstructor( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +) { + const id = path.call(print, "id"); + const parameters = [ + group([ + indent( + join( + ", ", + path.map((arg) => [softline, print(arg)], "parameters") + ) + ), + softline, + ]), ]; + return ["init ", id, "(", parameters, ")"]; } export function printNamespaceStatement( diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index ebc08756c4..53b6321bbc 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -817,6 +817,37 @@ scalar Foo @some @decorator scalar Foo; +`, + }); + }); + + it("format with constructors", async () => { + await assertFormat({ + code: ` +scalar + Foo { init fromFoo( + value: string)} +`, + expected: ` +scalar Foo { + init fromFoo(value: string); +} +`, + }); + }); + it("format with multiple constructors", async () => { + await assertFormat({ + code: ` +scalar + Foo { init fromFoo( + value: string); init fromBar( + value: string, other: string)} +`, + expected: ` +scalar Foo { + init fromFoo(value: string); + init fromBar(value: string, other: string); +} `, }); }); @@ -1012,6 +1043,42 @@ model Foo { }); }); + it("format empty scalar with comment inside", async () => { + await assertFormat({ + code: ` +scalar foo { + // empty scalar + + +} +`, + expected: ` +scalar foo { + // empty scalar +} +`, + }); + + await assertFormat({ + code: ` +scalar foo { + // empty scalar 1 + + + // empty scalar 2 + + +} +`, + expected: ` +scalar foo { + // empty scalar 1 + // empty scalar 2 +} +`, + }); + }); + it("format empty anonymous model with comment inside", async () => { await assertFormat({ code: ` diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 740dfa6e9e..cddb5b37b1 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -162,12 +162,15 @@ describe("compiler: parser", () => { `namespace Foo { scalar uuid extends string;} `, + `scalar uuid { + init fromString(def: string) + }`, + `scalar bar extends uuid { + init fromOther(abc: string) + }`, ]); - parseErrorEach([ - ["scalar uuid extends string { }", [/Statement expected./]], - ["scalar uuid is string;", [/Statement expected./]], - ]); + parseErrorEach([["scalar uuid is string;", [{ message: "';', or '{' expected." }]]]); }); describe("interface statements", () => { From dfb28746fe4bb6f86cc95e3bb6a670d7129f1dbf Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 13:47:18 -0700 Subject: [PATCH 047/184] Add colorization for scalar consutructors --- grammars/typespec.json | 93 ++++++++++++++++++- packages/compiler/src/server/classify.ts | 7 +- packages/compiler/src/server/tmlanguage.ts | 78 ++++++++++++---- .../compiler/test/server/colorization.test.ts | 30 ++++++ 4 files changed, 186 insertions(+), 22 deletions(-) diff --git a/grammars/typespec.json b/grammars/typespec.json index 2b6937a49a..66e881ff6d 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -34,7 +34,7 @@ }, "augment-decorator-statement": { "name": "meta.augment-decorator-statement.typespec", - "begin": "((@@)\\b[_$[:alpha:]]([_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)", + "begin": "((@@)\\b[_$[:alpha:]](?:[_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)", "beginCaptures": { "1": { "name": "entity.name.tag.tsp" @@ -62,6 +62,35 @@ "name": "constant.language.tsp", "match": "\\b(true|false)\\b" }, + "callExpression": { + "name": "meta.callExpression.typespec", + "begin": "(\\b[_$[:alpha:]](?:[_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "entity.name.function.tsp" + }, + "2": { + "name": "punctuation.parenthesis.open.tsp" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.parenthesis.close.tsp" + } + }, + "patterns": [ + { + "include": "#token" + }, + { + "include": "#expression" + }, + { + "include": "#punctuation-comma" + } + ] + }, "const-statement": { "name": "meta.const-statement.typespec", "begin": "\\b(const)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", @@ -88,7 +117,7 @@ }, "decorator": { "name": "meta.decorator.typespec", - "begin": "((@)\\b[_$[:alpha:]]([_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)", + "begin": "((@)\\b[_$[:alpha:]](?:[_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)", "beginCaptures": { "1": { "name": "entity.name.tag.tsp" @@ -367,6 +396,9 @@ { "include": "#model-expression" }, + { + "include": "#callExpression" + }, { "include": "#identifier-expression" } @@ -1034,6 +1066,56 @@ "name": "punctuation.terminator.statement.tsp", "match": ";" }, + "scalar-body": { + "name": "meta.scalar-body.typespec", + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.curlybrace.open.tsp" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.curlybrace.close.tsp" + } + }, + "patterns": [ + { + "include": "#token" + }, + { + "include": "#directive" + }, + { + "include": "#scalar-constructor" + }, + { + "include": "#punctuation-semicolon" + } + ] + }, + "scalar-constructor": { + "name": "meta.scalar-constructor.typespec", + "begin": "\\b(init)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "beginCaptures": { + "1": { + "name": "keyword.other.tsp" + }, + "2": { + "name": "entity.name.function.tsp" + } + }, + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "patterns": [ + { + "include": "#token" + }, + { + "include": "#operation-parameters" + } + ] + }, "scalar-extends": { "name": "meta.scalar-extends.typespec", "begin": "\\b(extends)\\b", @@ -1054,10 +1136,13 @@ }, "scalar-statement": { "name": "meta.scalar-statement.typespec", - "begin": "\\b(scalar)\\b", + "begin": "\\b(scalar)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" + }, + "2": { + "name": "entity.name.type.tsp" } }, "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", @@ -1072,7 +1157,7 @@ "include": "#scalar-extends" }, { - "include": "#expression" + "include": "#scalar-body" } ] }, diff --git a/packages/compiler/src/server/classify.ts b/packages/compiler/src/server/classify.ts index 863942df0a..9693221f8a 100644 --- a/packages/compiler/src/server/classify.ts +++ b/packages/compiler/src/server/classify.ts @@ -216,6 +216,9 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] { case SyntaxKind.ScalarStatement: classify(node.id, SemanticTokenKind.Type); break; + case SyntaxKind.ScalarConstructor: + classify(node.id, SemanticTokenKind.Function); + break; case SyntaxKind.EnumStatement: classify(node.id, SemanticTokenKind.Enum); break; @@ -253,7 +256,9 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] { case SyntaxKind.DecoratorExpression: classifyReference(node.target, SemanticTokenKind.Macro); break; - + case SyntaxKind.CallExpression: + classifyReference(node.target, SemanticTokenKind.Function); + break; case SyntaxKind.TypeReference: classifyReference(node.target); break; diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index f02f7420cd..d341df58ef 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -63,7 +63,7 @@ const beforeIdentifier = `(?=${identifierStart})`; const escapedIdentifier = "`(?:[^`\\\\]|\\\\.)*`"; const simpleIdentifier = `\\b${identifierStart}${identifierContinue}*\\b`; const identifier = `${simpleIdentifier}|${escapedIdentifier}`; -const qualifiedIdentifier = `\\b${identifierStart}(${identifierContinue}|\\.${identifierStart})*\\b`; +const qualifiedIdentifier = `\\b${identifierStart}(?:${identifierContinue}|\\.${identifierStart})*\\b`; const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"'; const modifierKeyword = `\\b(?:extern)\\b`; const statementKeyword = `\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b`; @@ -267,6 +267,21 @@ const parenthesizedExpression: BeginEndRule = { patterns: [expression, punctuationComma], }; +const callExpression: BeginEndRule = { + key: "callExpression", + scope: meta, + begin: `(${qualifiedIdentifier})\\s*(\\()`, + beginCaptures: { + "1": { scope: "entity.name.function.tsp" }, + "2": { scope: "punctuation.parenthesis.open.tsp" }, + }, + end: "\\)", + endCaptures: { + "0": { scope: "punctuation.parenthesis.close.tsp" }, + }, + patterns: [token, expression, punctuationComma], +}; + const decorator: BeginEndRule = { key: "decorator", scope: meta, @@ -520,6 +535,20 @@ const modelStatement: BeginEndRule = { ], }; +const operationParameters: BeginEndRule = { + key: "operation-parameters", + scope: meta, + begin: "\\(", + beginCaptures: { + "0": { scope: "punctuation.parenthesis.open.tsp" }, + }, + end: "\\)", + endCaptures: { + "0": { scope: "punctuation.parenthesis.close.tsp" }, + }, + patterns: [token, decorator, modelProperty, spreadExpression, punctuationComma], +}; + const scalarExtends: BeginEndRule = { key: "scalar-extends", scope: meta, @@ -531,19 +560,46 @@ const scalarExtends: BeginEndRule = { patterns: [expression, punctuationComma], }; +const scalarConstructor: BeginEndRule = { + key: "scalar-constructor", + scope: meta, + begin: `\\b(init)\\b\\s+(${identifier})`, + beginCaptures: { + "1": { scope: "keyword.other.tsp" }, + "2": { scope: "entity.name.function.tsp" }, + }, + end: universalEnd, + patterns: [token, operationParameters], +}; + +const scalarBody: BeginEndRule = { + key: "scalar-body", + scope: meta, + begin: "\\{", + beginCaptures: { + "0": { scope: "punctuation.curlybrace.open.tsp" }, + }, + end: "\\}", + endCaptures: { + "0": { scope: "punctuation.curlybrace.close.tsp" }, + }, + patterns: [token, directive, scalarConstructor, punctuationSemicolon], +}; + const scalarStatement: BeginEndRule = { key: "scalar-statement", scope: meta, - begin: "\\b(scalar)\\b", + begin: `\\b(scalar)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, + "2": { scope: "entity.name.type.tsp" }, }, end: universalEnd, patterns: [ token, typeParameters, scalarExtends, // before expression or `extends` will look like type name - expression, // enough to match name, type parameters, and body. + scalarBody, ], }; @@ -633,6 +689,7 @@ const aliasStatement: BeginEndRule = { end: universalEnd, patterns: [typeParameters, operatorAssignment, expression], }; + const constStatement: BeginEndRule = { key: "const-statement", scope: meta, @@ -678,20 +735,6 @@ const namespaceStatement: BeginEndRule = { patterns: [token, namespaceName, namespaceBody], }; -const operationParameters: BeginEndRule = { - key: "operation-parameters", - scope: meta, - begin: "\\(", - beginCaptures: { - "0": { scope: "punctuation.parenthesis.open.tsp" }, - }, - end: "\\)", - endCaptures: { - "0": { scope: "punctuation.parenthesis.close.tsp" }, - }, - patterns: [token, decorator, modelProperty, spreadExpression, punctuationComma], -}; - const operationHeritage: BeginEndRule = { key: "operation-heritage", scope: meta, @@ -979,6 +1022,7 @@ expression.patterns = [ tupleLiteral, tupleExpression, modelExpression, + callExpression, identifierExpression, ]; diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index e5f4a6d2e8..93acfc85fa 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -33,6 +33,7 @@ const Token = { keywords: { model: createToken("model", "keyword.other.tsp"), scalar: createToken("scalar", "keyword.other.tsp"), + init: createToken("init", "keyword.other.tsp"), enum: createToken("enum", "keyword.other.tsp"), union: createToken("union", "keyword.other.tsp"), operation: createToken("op", "keyword.other.tsp"), @@ -749,6 +750,24 @@ function testColorization(description: string, tokenize: Tokenize) { Token.punctuation.semicolon, ]); }); + + it("scalar with constructor", async () => { + const tokens = await tokenize("scalar foo { init fromFoo(value: string); }"); + deepStrictEqual(tokens, [ + Token.keywords.scalar, + Token.identifiers.type("foo"), + Token.punctuation.openBrace, + Token.keywords.init, + Token.identifiers.functionName("fromFoo"), + Token.punctuation.openParen, + Token.identifiers.variable("value"), + Token.operators.typeAnnotation, + Token.identifiers.type("string"), + Token.punctuation.closeParen, + Token.punctuation.semicolon, + Token.punctuation.closeBrace, + ]); + }); }); it("named template argument list", async () => { @@ -1089,6 +1108,17 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); + describe("call expressions", () => { + it("without parameters", async () => { + const tokens = await tokenizeWithConst("foo()"); + deepStrictEqual(tokens, [ + Token.identifiers.functionName("foo"), + Token.punctuation.openParen, + Token.punctuation.closeParen, + ]); + }); + }); + describe("object literals", () => { it("empty", async () => { const tokens = await tokenizeWithConst("#{}"); From 10e69f91993e6a28c5a71198e7b579dc06b92456 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 13:54:55 -0700 Subject: [PATCH 048/184] update grammar --- packages/spec/src/spec.emu.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 1fa5128644..61d380be36 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -331,11 +331,25 @@

Syntactic Grammar

`is` Expression ScalarStatement : - DecoratorList? `scalar` Identifier TemplateParameters? ScalarExtends `;` + DecoratorList? `scalar` Identifier TemplateParameters? ScalarExtends? `;` + DecoratorList? `scalar` Identifier TemplateParameters? ScalarExtends? `{` ScalarBody? `}` ScalarExtends : `extends` Expression +ScalarBody : + ScalarMemberList `;`? + +ScalarBody : + ScalarMemberList `;`? + +ScalarMemberList : + ScalarMember + ScalarMemberList `;` ScalarMember + +ScalarMember: + `init` Identifier `(` FunctionParameterList? `)` + ExtendsModelHeritage : `extends` Expression From 4b29c93352708d1308e6bf4bdc33a9251f1c6f45 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 13:56:11 -0700 Subject: [PATCH 049/184] Delete .chronus/changes/feature-object-literals-2024-2-15-21-56-46.md --- .../changes/feature-object-literals-2024-2-15-21-56-46.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .chronus/changes/feature-object-literals-2024-2-15-21-56-46.md diff --git a/.chronus/changes/feature-object-literals-2024-2-15-21-56-46.md b/.chronus/changes/feature-object-literals-2024-2-15-21-56-46.md deleted file mode 100644 index c803cf6819..0000000000 --- a/.chronus/changes/feature-object-literals-2024-2-15-21-56-46.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: feature -packages: - - "@typespec/openapi3" ---- - -Add support for tuple literal in default values From cf59227ff991b5cd058a0e549e4f19645641a06d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 14:02:31 -0700 Subject: [PATCH 050/184] Fix --- packages/html-program-viewer/src/ui.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/html-program-viewer/src/ui.tsx b/packages/html-program-viewer/src/ui.tsx index 943fb26d5e..15a7981dd0 100644 --- a/packages/html-program-viewer/src/ui.tsx +++ b/packages/html-program-viewer/src/ui.tsx @@ -282,6 +282,7 @@ const ModelPropertyUI: FunctionComponent<{ type: ModelProperty }> = ({ type }) = optional: "value", sourceProperty: "ref", default: "value", + defaultValue: "value", }} /> ); From 63f46cb66721e24a8cdaeaa8082922e95f939c44 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 14:32:16 -0700 Subject: [PATCH 051/184] no decorator on scalar members --- packages/compiler/src/core/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index adec961516..ce09d9b9c6 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1088,7 +1088,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa pos: number, decorators: DecoratorExpressionNode[] ): ScalarConstructorNode { - reportInvalidDecorators(decorators, "spread property"); + reportInvalidDecorators(decorators, "scalar member"); parseExpected(Token.InitKeyword); const id = parseIdentifier(); From 26cda8a79a8cf69c0cb42d6e053e15276667e8e9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 10 Apr 2024 20:37:21 -0700 Subject: [PATCH 052/184] named constructor checking --- packages/compiler/lib/lib.tsp | 14 +- packages/compiler/src/core/checker.ts | 181 ++++++++++++++++-- packages/compiler/src/core/messages.ts | 25 +++ packages/compiler/src/core/semantic-walker.ts | 13 ++ packages/compiler/src/core/types.ts | 23 ++- packages/compiler/src/server/completion.ts | 8 + .../compiler/src/server/type-signature.ts | 3 +- 7 files changed, 247 insertions(+), 20 deletions(-) diff --git a/packages/compiler/lib/lib.tsp b/packages/compiler/lib/lib.tsp index 29a5e14c41..13d767908b 100644 --- a/packages/compiler/lib/lib.tsp +++ b/packages/compiler/lib/lib.tsp @@ -105,12 +105,22 @@ scalar plainTime; /** * An instant in coordinated universal time (UTC)" */ -scalar utcDateTime; +scalar utcDateTime { + /** + * Create a date from an ISO string. + */ + init fromISO(value: string); +} /** * A date and time in a particular time zone, e.g. "April 10th at 3:00am in PST" */ -scalar offsetDateTime; +scalar offsetDateTime { + /** + * Create a date from an ISO string. + */ + init fromISO(value: string); +} /** * A duration/time period. e.g 5s, 10h diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 677f362529..18b3c364f4 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -51,6 +51,7 @@ import { BooleanLiteral, BooleanLiteralNode, BooleanValue, + CallExpressionNode, CodeFix, ConstStatementNode, DecoratedType, @@ -132,6 +133,8 @@ import { ReturnExpressionNode, ReturnRecord, Scalar, + ScalarConstructor, + ScalarConstructorNode, ScalarStatementNode, StdTypeName, StdTypes, @@ -659,6 +662,8 @@ export function createChecker(program: Program): Checker { return checkOperation(node, mapper, containerType as Interface); case SyntaxKind.UnionVariant: return checkUnionVariant(node, mapper); + case SyntaxKind.ScalarConstructor: + return checkScalarConstructor(node, mapper, containerType as Scalar); } } @@ -685,13 +690,15 @@ export function createChecker(program: Program): Checker { case SyntaxKind.ConstStatement: return checkConst(node); case SyntaxKind.StringLiteral: - return checkStringValue(node); + return checkStringValue(node, undefined); case SyntaxKind.NumericLiteral: - return checkNumericValue(node); + return checkNumericValue(node, undefined); case SyntaxKind.BooleanLiteral: - return checkBooleanValue(node); + return checkBooleanValue(node, undefined); case SyntaxKind.TypeReference: return checkValueReference(node, mapper); + case SyntaxKind.CallExpression: + return checkCallExpression(node, mapper); default: reportCheckerDiagnostic( createDiagnostic({ @@ -736,11 +743,17 @@ export function createChecker(program: Program): Checker { case SyntaxKind.OperationStatement: return checkOperation(node, mapper); case SyntaxKind.NumericLiteral: - return constraint?.kind === "Value" ? checkNumericValue(node) : checkNumericLiteral(node); + return constraint?.kind === "Value" + ? checkNumericValue(node, undefined) + : checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: - return constraint?.kind === "Value" ? checkBooleanValue(node) : checkBooleanLiteral(node); + return constraint?.kind === "Value" + ? checkBooleanValue(node, undefined) + : checkBooleanLiteral(node); case SyntaxKind.StringLiteral: - return constraint?.kind === "Value" ? checkStringValue(node) : checkStringLiteral(node); + return constraint?.kind === "Value" + ? checkStringValue(node, undefined) + : checkStringLiteral(node); case SyntaxKind.TupleExpression: return checkTupleExpression(node, mapper); case SyntaxKind.StringTemplateExpression: @@ -3243,30 +3256,30 @@ export function createChecker(program: Program): Checker { }; } - function checkStringValue(node: StringLiteralNode): StringValue { + function checkStringValue(node: StringLiteralNode, scalar: Scalar | undefined): StringValue { return { valueKind: "StringValue", value: node.value, type: getLiteralType(node), - scalar: undefined, + scalar, }; } - function checkNumericValue(node: NumericLiteralNode): NumericValue { + function checkNumericValue(node: NumericLiteralNode, scalar: Scalar | undefined): NumericValue { return { valueKind: "NumericValue", value: Numeric(node.valueAsString), type: getLiteralType(node), - scalar: undefined, + scalar, }; } - function checkBooleanValue(node: BooleanLiteralNode): BooleanValue { + function checkBooleanValue(node: BooleanLiteralNode, scalar: Scalar | undefined): BooleanValue { return { valueKind: "BooleanValue", value: node.value, type: getLiteralType(node), - scalar: undefined, + scalar, }; } @@ -3290,6 +3303,82 @@ export function createChecker(program: Program): Checker { return value; } + // TODO: should those be called eval? + function checkCallExpression( + node: CallExpressionNode, + mapper: TypeMapper | undefined + ): Value | undefined { + const target = checkTypeReference(node.target, mapper); + if (target.kind !== "Scalar" && target.kind !== "ScalarConstructor") { + reportCheckerDiagnostic( + createDiagnostic({ + code: "non-callable", + format: { typeKind: target.kind }, + target: node.target, + }) + ); + return undefined; + } + + if (target.kind === "ScalarConstructor") { + const args = node.arguments.map((x) => getValueForNode(x, mapper)).filter(isDefined); + return { + valueKind: "ScalarValue", + value: { + name: "abc", + args, + }, + scalar: target.scalar, + type: target, + }; + } + + const checkPrimitiveArg = (kind: T): (Node & { kind: T }) | undefined => { + if (node.arguments.length !== 1) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-primitive-init", + target: node.target, + }) + ); + return undefined; + } + const arg = node.arguments[0]; + if (arg.kind !== kind) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-primitive-init", + messageId: "invalidArg", + format: { actual: SyntaxKind[arg.kind], expected: SyntaxKind[kind] }, + target: arg, + }) + ); + return undefined; + } + return arg as any; + }; + + if (areScalarsRelated(target, getStdType("string"))) { + const arg = checkPrimitiveArg(SyntaxKind.StringLiteral); + return arg ? checkStringValue(arg, target) : undefined; + } else if (areScalarsRelated(target, getStdType("numeric"))) { + const arg = checkPrimitiveArg(SyntaxKind.NumericLiteral); + return arg ? checkNumericValue(arg, target) : undefined; + } else if (areScalarsRelated(target, getStdType("boolean"))) { + const arg = checkPrimitiveArg(SyntaxKind.BooleanLiteral); + return arg ? checkBooleanValue(arg, target) : undefined; + } else { + reportCheckerDiagnostic( + createDiagnostic({ + code: "named-init-required", + format: { typeKind: target.kind }, + target: node.target, + }) + ); + return undefined; + } + } + function createUnion(options: Type[]): Union { const variants = createRekeyableMap(); const union: Union = createAndFinishType({ @@ -3388,6 +3477,15 @@ export function createChecker(program: Program): Checker { } } break; + case SyntaxKind.ScalarStatement: + if (node.extends && node.extends.kind === SyntaxKind.TypeReference) { + resolveAndCopyMembers(node.extends); + } + for (const member of node.members) { + const name = member.id.sv; + bindMember(name, member, SymbolFlags.ScalarMember); + } + break; case SyntaxKind.ModelExpression: for (const prop of node.properties) { if (prop.kind === SyntaxKind.ModelSpreadProperty) { @@ -3594,6 +3692,11 @@ export function createChecker(program: Program): Checker { lateBindMember(prop, SymbolFlags.ModelProperty); } break; + case "Scalar": + for (const member of type.constructors.values()) { + lateBindMember(member, SymbolFlags.Member); + } + break; case "Enum": for (const member of type.members.values()) { lateBindMember(member, SymbolFlags.EnumMember); @@ -4262,10 +4365,12 @@ export function createChecker(program: Program): Checker { kind: "Scalar", name: node.id.sv, node: node, + constructors: new Map(), namespace: getParentNamespaceType(node), decorators, derivedScalars: [], }); + checkScalarConstructors(type, node, type.constructors, mapper); linkType(links, type, mapper); if (node.extends) { @@ -4333,6 +4438,58 @@ export function createChecker(program: Program): Checker { return extendsType; } + function checkScalarConstructors( + parentScalar: Scalar, + node: ScalarStatementNode, + constructors: Map, + mapper: TypeMapper | undefined + ) { + for (const member of node.members) { + const constructor = checkScalarConstructor(member, mapper, parentScalar); + if (constructors.has(constructor.name as string)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "constructor-duplicate", + format: { name: constructor.name.toString() }, + target: member, + }) + ); + continue; + } + constructors.set(constructor.name, constructor); + } + } + + function checkScalarConstructor( + node: ScalarConstructorNode, + mapper: TypeMapper | undefined, + parentScalar: Scalar + ): ScalarConstructor { + const name = node.id.sv; + const links = getSymbolLinksForMember(node); + if (links && links.declaredType && mapper === undefined) { + // we're not instantiating this union variant and we've already checked it + return links.declaredType as ScalarConstructor; + } + + const member: ScalarConstructor = createType({ + kind: "ScalarConstructor", + scalar: parentScalar, + name, + node, + parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper)), + }); + linkMapper(member, mapper); + if (shouldCreateTypeForTemplate(node.parent!, mapper)) { + finishType(member); + } + if (links) { + linkType(links, member, mapper); + } + + return finishType(member); + } + function checkAlias(node: AliasStatementNode, mapper: TypeMapper | undefined): Type { const links = getSymbolLinks(node.symbol); diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 0170e9314f..5c8ef708a4 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -410,6 +410,25 @@ const diagnostics = { default: paramMessage`${"name"} refers to a type, but is being used as a value here.`, }, }, + "non-callable": { + severity: "error", + messages: { + default: paramMessage`Type ${"typeKind"} is not is not callable.`, + }, + }, + "named-init-required": { + severity: "error", + messages: { + default: paramMessage`Only scalar deriving from 'string', 'numeric' or 'boolean' can be instantited without a named constructor.`, + }, + }, + "invalid-primitive-init": { + severity: "error", + messages: { + default: `Instantiating scalar deriving from 'string', 'numeric' or 'boolean' can only take a single argument.`, + invalidArg: paramMessage`Expected a single argument of type ${"expected"} but got ${"actual"}.`, + }, + }, unassignable: { severity: "error", messages: { @@ -477,6 +496,12 @@ const diagnostics = { default: paramMessage`Enum already has a member named ${"name"}`, }, }, + "constructor-duplicate": { + severity: "error", + messages: { + default: paramMessage`A constructor already exists with name ${"name"}`, + }, + }, "spread-enum": { severity: "error", messages: { diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index f704d1702f..e84ffadb78 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -10,6 +10,7 @@ import { Namespace, Operation, Scalar, + ScalarConstructor, SemanticNodeListener, StringTemplate, StringTemplateSpan, @@ -266,6 +267,9 @@ function navigateScalarType(scalar: Scalar, context: NavigationContext) { if (scalar.baseScalar) { navigateScalarType(scalar.baseScalar, context); } + for (const constructor of scalar.constructors.values()) { + navigateScalarConstructor(constructor, context); + } context.emit("exitScalar", scalar); } @@ -353,6 +357,13 @@ function navigateDecoratorDeclaration(type: Decorator, context: NavigationContex if (context.emit("decorator", type) === ListenerFlow.NoRecursion) return; } +function navigateScalarConstructor(type: ScalarConstructor, context: NavigationContext) { + if (checkVisited(context.visited, type)) { + return; + } + if (context.emit("scalarConstructor", type) === ListenerFlow.NoRecursion) return; +} + function navigateTypeInternal(type: Type, context: NavigationContext) { switch (type.kind) { case "Model": @@ -383,6 +394,8 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { return navigateTemplateParameter(type, context); case "Decorator": return navigateDecoratorDeclaration(type, context); + case "ScalarConstructor": + return navigateScalarConstructor(type, context); case "Object": case "Projection": case "Function": diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 996143e940..583f1f2324 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -117,6 +117,7 @@ export type Type = | Operation | Projection | Scalar + | ScalarConstructor | StringLiteral | StringTemplate | StringTemplateSpan @@ -387,13 +388,23 @@ export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { */ derivedScalars: Scalar[]; + constructors: Map; /** - * Late-bound symbol of this model type. + * Late-bound symbol of this scalar type. * @internal */ symbol?: Sym; } +// TODO: should we just call that Constructor or NamedConstructor for future proof? +export interface ScalarConstructor extends BaseType { + kind: "ScalarConstructor"; + node: ScalarConstructorNode; + name: string; + scalar: Scalar; + parameters: FunctionParameter[]; +} + export interface Interface extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Interface"; name: string; @@ -769,18 +780,19 @@ export const enum SymbolFlags { Declaration = 1 << 22, Implementation = 1 << 23, Const = 1 << 24, + ScalarMember = 1 << 25, /** * A symbol which was late-bound, in which case, the type referred to * by this symbol is stored directly in the symbol. */ - LateBound = 1 << 25, + LateBound = 1 << 26, ExportContainer = Namespace | SourceFile, /** * Symbols whose members will be late bound (and stored on the type) */ - MemberContainer = Model | Enum | Union | Interface, + MemberContainer = Model | Enum | Union | Interface | Scalar, Member = ModelProperty | EnumMember | UnionVariant | InterfaceMember, } @@ -1039,9 +1051,10 @@ export type MemberNode = | ModelPropertyNode | EnumMemberNode | OperationStatementNode - | UnionVariantNode; + | UnionVariantNode + | ScalarConstructorNode; -export type MemberContainerType = Model | Enum | Interface | Union; +export type MemberContainerType = Model | Enum | Interface | Union | Scalar; /** * Type that can be used as members of a container type. diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 4e5876ee02..af91bebfe7 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -53,6 +53,9 @@ export async function resolveCompletion( case SyntaxKind.NamespaceStatement: addKeywordCompletion("namespace", completions); break; + case SyntaxKind.ScalarStatement: + addKeywordCompletion("scalar", completions); + break; case SyntaxKind.Identifier: addDirectiveCompletion(context, node); addIdentifierCompletion(context, node); @@ -72,6 +75,7 @@ interface KeywordArea { namespace?: boolean; model?: boolean; identifier?: boolean; + scalar?: boolean; } const keywords = [ @@ -90,6 +94,7 @@ const keywords = [ ["op", { root: true, namespace: true }], ["dec", { root: true, namespace: true }], ["fn", { root: true, namespace: true }], + ["const", { root: true, namespace: true }], // On model `model Foo ...` ["extends", { model: true }], @@ -104,6 +109,9 @@ const keywords = [ // Modifiers ["extern", { root: true, namespace: true }], + + // Scalars + ["init", { scalar: true }], ] as const; function addKeywordCompletion(area: keyof KeywordArea, completions: CompletionList) { diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index 7ee0480702..74df59bab9 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -39,9 +39,10 @@ function getTypeSignature(type: Type | ValueType): string { case "Model": case "Namespace": return fence(`${type.kind.toLowerCase()} ${getPrintableTypeName(type)}`); + case "ScalarConstructor": + return fence(`init ${getTypeSignature(type.scalar)}.${type.name}`); case "Decorator": return fence(getDecoratorSignature(type)); - case "Function": return fence(getFunctionSignature(type)); case "Operation": From e2a4787ee77f3a287978066a0d03cfe8f2a3e51b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 07:37:18 -0700 Subject: [PATCH 053/184] Compare using Numeric --- packages/compiler/src/core/checker.ts | 44 ++++++++++++------- .../src/core/helpers/type-name-utils.ts | 1 + packages/compiler/src/core/types.ts | 3 +- .../compiler/test/checker/relation.test.ts | 3 +- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 18b3c364f4..1b05c0377c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5842,6 +5842,7 @@ export function createChecker(program: Program): Checker { kind: "Number", value, valueAsString, + numericValue: Numeric(valueAsString), }); break; } @@ -6410,6 +6411,10 @@ export function createChecker(program: Program): Checker { } function isNumericLiteralRelatedTo(source: NumericLiteral, target: Scalar) { + return isNumericAssignableToNumericScalar(source.numericValue, target); + } + + function isNumericAssignableToNumericScalar(source: Numeric, target: Scalar) { // if the target does not derive from numeric, then it can't be assigned a numeric literal if (!areScalarsRelated(target, getStdType("numeric"))) { return false; @@ -6429,13 +6434,17 @@ export function createChecker(program: Program): Checker { if (target.name === "decimal") return true; if (target.name === "decimal128") return true; - const isInt = Number.isInteger(source.value); + const isInt = source.isInteger; if (target.name === "integer") return isInt; if (target.name === "float") return true; if (!(target.name in numericRanges)) return false; const [low, high, options] = numericRanges[target.name]; - return source.value >= low && source.value <= high && (!options.int || isInt); + return ( + source.gte(Numeric(low.toString())) && + source.lte(Numeric(high.toString())) && + (!options.int || isInt) + ); } function isModelRelatedTo( @@ -6791,21 +6800,22 @@ function isAnonymous(type: Type) { return !("name" in type) || typeof type.name !== "string" || !type.name; } -const numericRanges: Record< - string, - [min: number | bigint, max: number | bigint, options: { int: boolean }] -> = { - int64: [BigInt("-9223372036854775807"), BigInt("9223372036854775808"), { int: true }], - int32: [-2147483648, 2147483647, { int: true }], - int16: [-32768, 32767, { int: true }], - int8: [-128, 127, { int: true }], - uint64: [0, BigInt("18446744073709551615"), { int: true }], - uint32: [0, 4294967295, { int: true }], - uint16: [0, 65535, { int: true }], - uint8: [0, 255, { int: true }], - safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, { int: true }], - float32: [-3.4e38, 3.4e38, { int: false }], - float64: [-Number.MAX_VALUE, Number.MAX_VALUE, { int: false }], +const numericRanges: Record = { + int64: [Numeric("-9223372036854775807"), Numeric("9223372036854775808"), { int: true }], + int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true }], + int16: [Numeric("-32768"), Numeric("32767"), { int: true }], + int8: [Numeric("-128"), Numeric("127"), { int: true }], + uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true }], + uint32: [Numeric("0"), Numeric("4294967295"), { int: true }], + uint16: [Numeric("0"), Numeric("65535"), { int: true }], + uint8: [Numeric("0"), Numeric("255"), { int: true }], + safeint: [ + Numeric(Number.MIN_SAFE_INTEGER.toString()), + Numeric(Number.MAX_SAFE_INTEGER.toString()), + { int: true }, + ], + float32: [Numeric("-3.4e38"), Numeric("3.4e38"), { int: false }], + float64: [Numeric(`${-Number.MAX_VALUE}`), Numeric(Number.MAX_VALUE.toString()), { int: false }], }; /** diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 184c6a8210..ad626aecc6 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -51,6 +51,7 @@ export function getTypeName(type: Type, options?: TypeNameOptions): string { case "String": return `"${type.value}"`; case "Number": + return type.valueAsString; case "Boolean": return type.value.toString(); case "Intrinsic": diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 583f1f2324..a80272cbe4 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -561,7 +561,8 @@ export interface StringLiteral extends BaseType { export interface NumericLiteral extends BaseType { kind: "Number"; node?: NumericLiteralNode; - value: number; + value: number; // TODO: should we deprecate this? + numericValue: Numeric; valueAsString: string; } diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 2763d5258f..92c461b71c 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -458,8 +458,7 @@ describe("compiler: checker: type relations", () => { }); }); - // Need to handle bigint in TypeSpec. https://github.com/Azure/typespec-azure/issues/506 - describe.skip("int64 target", () => { + describe("int64 target", () => { it("can assign int64", async () => { await expectTypeAssignable({ source: "int64", target: "int64" }); }); From 08fb196782fce4a6ecd4b94d0ae2f1b088e40221 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 07:44:43 -0700 Subject: [PATCH 054/184] min --- packages/compiler/src/core/checker.ts | 81 ++++++++++++++++----------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 1b05c0377c..c18eb35e9b 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3303,13 +3303,14 @@ export function createChecker(program: Program): Checker { return value; } - // TODO: should those be called eval? - function checkCallExpression( + function checkCallExpressionTarget( node: CallExpressionNode, mapper: TypeMapper | undefined - ): Value | undefined { + ): ScalarConstructor | Scalar | undefined { const target = checkTypeReference(node.target, mapper); - if (target.kind !== "Scalar" && target.kind !== "ScalarConstructor") { + if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { + return target; + } else { reportCheckerDiagnostic( createDiagnostic({ code: "non-callable", @@ -3319,13 +3320,52 @@ export function createChecker(program: Program): Checker { ); return undefined; } + } + + /** Check the arguments of the call expression are a single value of the given syntax. */ + function checkPrimitiveArg( + node: CallExpressionNode, + kind: T + ): (Node & { kind: T }) | undefined { + if (node.arguments.length !== 1) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-primitive-init", + target: node.target, + }) + ); + return undefined; + } + const arg = node.arguments[0]; + if (arg.kind !== kind) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-primitive-init", + messageId: "invalidArg", + format: { actual: SyntaxKind[arg.kind], expected: SyntaxKind[kind] }, + target: arg, + }) + ); + return undefined; + } + return arg as any; + } + // TODO: should those be called eval? + function checkCallExpression( + node: CallExpressionNode, + mapper: TypeMapper | undefined + ): Value | undefined { + const target = checkCallExpressionTarget(node, mapper); + if (target === undefined) { + return; + } if (target.kind === "ScalarConstructor") { const args = node.arguments.map((x) => getValueForNode(x, mapper)).filter(isDefined); return { valueKind: "ScalarValue", value: { - name: "abc", + name: target.name, args, }, scalar: target.scalar, @@ -3333,39 +3373,14 @@ export function createChecker(program: Program): Checker { }; } - const checkPrimitiveArg = (kind: T): (Node & { kind: T }) | undefined => { - if (node.arguments.length !== 1) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "invalid-primitive-init", - target: node.target, - }) - ); - return undefined; - } - const arg = node.arguments[0]; - if (arg.kind !== kind) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "invalid-primitive-init", - messageId: "invalidArg", - format: { actual: SyntaxKind[arg.kind], expected: SyntaxKind[kind] }, - target: arg, - }) - ); - return undefined; - } - return arg as any; - }; - if (areScalarsRelated(target, getStdType("string"))) { - const arg = checkPrimitiveArg(SyntaxKind.StringLiteral); + const arg = checkPrimitiveArg(node, SyntaxKind.StringLiteral); return arg ? checkStringValue(arg, target) : undefined; } else if (areScalarsRelated(target, getStdType("numeric"))) { - const arg = checkPrimitiveArg(SyntaxKind.NumericLiteral); + const arg = checkPrimitiveArg(node, SyntaxKind.NumericLiteral); return arg ? checkNumericValue(arg, target) : undefined; } else if (areScalarsRelated(target, getStdType("boolean"))) { - const arg = checkPrimitiveArg(SyntaxKind.BooleanLiteral); + const arg = checkPrimitiveArg(node, SyntaxKind.BooleanLiteral); return arg ? checkBooleanValue(arg, target) : undefined; } else { reportCheckerDiagnostic( From 56bbb6ed9437a6a3ad3159ae1f32f85c77336297 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 09:03:09 -0700 Subject: [PATCH 055/184] Progress with tests --- packages/compiler/src/core/checker.ts | 59 ++++-- packages/compiler/src/core/numeric.ts | 7 + packages/compiler/test/checker/values.test.ts | 175 ++++++++++++++++++ 3 files changed, 223 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c18eb35e9b..ad330f2ae0 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -788,6 +788,8 @@ export function createChecker(program: Program): Checker { return checkTupleLiteral(node, mapper); case SyntaxKind.ConstStatement: return checkConst(node) ?? errorType; // TODO: do we want that? + case SyntaxKind.CallExpression: + return checkCallExpression(node, mapper) ?? errorType; // TODO: do we want that? default: return errorType; } @@ -3323,10 +3325,11 @@ export function createChecker(program: Program): Checker { } /** Check the arguments of the call expression are a single value of the given syntax. */ - function checkPrimitiveArg( + function checkPrimitiveArg( node: CallExpressionNode, - kind: T - ): (Node & { kind: T }) | undefined { + scalar: Scalar, + valueKind: T["valueKind"] + ): T | undefined { if (node.arguments.length !== 1) { reportCheckerDiagnostic( createDiagnostic({ @@ -3336,19 +3339,26 @@ export function createChecker(program: Program): Checker { ); return undefined; } - const arg = node.arguments[0]; - if (arg.kind !== kind) { + const argNode = node.arguments[0]; + const value = getValueForNode(argNode, undefined); + if (value === undefined) { + return undefined; // error should already have been reported above. + } + if (value.valueKind !== valueKind) { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-primitive-init", messageId: "invalidArg", - format: { actual: SyntaxKind[arg.kind], expected: SyntaxKind[kind] }, - target: arg, + format: { actual: value.valueKind, expected: valueKind }, + target: argNode, }) ); return undefined; } - return arg as any; + if (!checkValueOfType(value, scalar, argNode)) { + return undefined; + } + return { ...value, scalar } as any; } // TODO: should those be called eval? @@ -3374,14 +3384,11 @@ export function createChecker(program: Program): Checker { } if (areScalarsRelated(target, getStdType("string"))) { - const arg = checkPrimitiveArg(node, SyntaxKind.StringLiteral); - return arg ? checkStringValue(arg, target) : undefined; + return checkPrimitiveArg(node, target, "StringValue"); } else if (areScalarsRelated(target, getStdType("numeric"))) { - const arg = checkPrimitiveArg(node, SyntaxKind.NumericLiteral); - return arg ? checkNumericValue(arg, target) : undefined; + return checkPrimitiveArg(node, target, "NumericValue"); } else if (areScalarsRelated(target, getStdType("boolean"))) { - const arg = checkPrimitiveArg(node, SyntaxKind.BooleanLiteral); - return arg ? checkBooleanValue(arg, target) : undefined; + return checkPrimitiveArg(node, target, "BooleanValue"); } else { reportCheckerDiagnostic( createDiagnostic({ @@ -6020,6 +6027,18 @@ export function createChecker(program: Program): Checker { return related; } + function checkValueOfType( + source: Value, + target: Type, + diagnosticTarget: DiagnosticTarget + ): boolean { + const [related, diagnostics] = isValueOfType(source, target, diagnosticTarget); + if (!related) { + reportCheckerDiagnostics(diagnostics); + } + return related; + } + /** * Check if the source type can be assigned to the target type. * @param source Source type @@ -6455,11 +6474,15 @@ export function createChecker(program: Program): Checker { if (!(target.name in numericRanges)) return false; const [low, high, options] = numericRanges[target.name]; - return ( - source.gte(Numeric(low.toString())) && - source.lte(Numeric(high.toString())) && - (!options.int || isInt) + console.log( + "HJEre", + low.toString(), + high.toString(), + source.toString(), + source.gte(low), + source.lte(high) ); + return source.gte(low) && source.lte(high) && (!options.int || isInt); } function isModelRelatedTo( diff --git a/packages/compiler/src/core/numeric.ts b/packages/compiler/src/core/numeric.ts index 28a455aa74..1a6c142cda 100644 --- a/packages/compiler/src/core/numeric.ts +++ b/packages/compiler/src/core/numeric.ts @@ -36,6 +36,13 @@ export class InvalidNumericError extends Error { /** @internal */ export const InternalDataSym = Symbol.for("NumericInternalData"); +/** + * Check if the given arg is a Numeric + */ +export function isNumeric(arg: unknown): arg is Numeric { + return typeof arg === "object" && arg !== null && InternalDataSym in arg; +} + /** * Represent any possible numeric value */ diff --git a/packages/compiler/test/checker/values.test.ts b/packages/compiler/test/checker/values.test.ts index aaff87fbc8..464cd8a7dc 100644 --- a/packages/compiler/test/checker/values.test.ts +++ b/packages/compiler/test/checker/values.test.ts @@ -272,3 +272,178 @@ describe("tuple literals", () => { }); }); }); + +describe("numeric literals", () => { + describe("instantiate from numeric literal", () => { + it.each([ + "numeric", + // Integers + "integer", + "int8", + "int16", + "int32", + "int64", + "safeint", + "uint8", + "uint16", + "uint32", + "uint64", + // Floats + "float", + "float32", + "float64", + // Decimals + "decimal", + "decimal128", + ])("%s", async (scalarName) => { + const value = await compileValueType(`${scalarName}(123)`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.scalar?.name, scalarName); + strictEqual(value.value.asNumber(), 123); + }); + }); + + describe("validate numeric literal is assignable", () => { + // it.each([ + // // numeric + // ["1234", "int8"], + // ["-12", "uint8"], + // ["1234", "int16"], + // ["-21", "uint16"], + // ["-12", "uint32"], + // ["1234", "int32"], + // ["-12", "uint64"], + // ["1234", "int64"], + // ])("%s ⇏ %s", async (a, b) => { + // const { diagnostics, pos } = await diagnoseUsage(` + // const a = ${b}(┆${a}); + // `); + // expectDiagnostics(diagnostics, { + // code: "unassignable", + // message: `Type '${a}' is not assignable to type '${b}'`, + // pos, + // }); + // }); + const cases = [ + [ + "int8", + [ + ["✔", "123"], + ["✔", "-123"], + ["✘", "1234"], + ["✘", "-1234"], + ], + ], + ] as const; + describe.each(cases)("%s", (scalarName, perScalarCases) => { + it.each(perScalarCases)("%s %s", async (pass, literal) => { + const { diagnostics, pos } = await diagnoseUsage(` + const a = ${scalarName}(┆${literal}); + `); + if (pass === "✔") { + expectDiagnosticEmpty(diagnostics); + } else { + expectDiagnostics(diagnostics, { + code: "unassignable", + message: `Type '${literal}' is not assignable to type '${scalarName}'`, + pos, + }); + } + }); + }); + }); + + describe("instantiate from another smaller numeric type", () => { + it.each([ + // int8 + ["int8", "int8"], + ["int8", "int16"], + ["int8", "int32"], + ["int8", "int64"], + ["int8", "integer"], + ["int8", "numeric"], + // uint8 + ["uint8", "int16"], + ["uint8", "int32"], + ["uint8", "int64"], + ["uint8", "integer"], + ["uint8", "numeric"], + // int32 + ["int32", "int32"], + ["int32", "int64"], + ["int32", "integer"], + ["int32", "numeric"], + // uint32 + ["uint32", "int64"], + ["uint32", "integer"], + ["uint32", "numeric"], + ])("%s → %s", async (a, b) => { + const value = await compileValueType(`${b}(${a}(123))`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.scalar?.name, b); + strictEqual(value.value.asNumber(), 123); + }); + }); + + describe("cannot instantiate from a larger numeric type", () => { + it.each([ + // numeric + ["numeric", "integer"], + ["numeric", "int8"], + ["numeric", "int16"], + ["numeric", "int32"], + ["numeric", "int64"], + ["numeric", "safeint"], + ["numeric", "uint8"], + ["numeric", "uint16"], + ["numeric", "uint32"], + ["numeric", "uint64"], + ["numeric", "float"], + ["numeric", "float32"], + ["numeric", "float64"], + ["numeric", "decimal"], + ["numeric", "decimal128"], + + // float32 + ["float32", "integer"], + ["numeric", "int8"], + ["numeric", "int16"], + ["numeric", "int32"], + ["numeric", "int64"], + ["numeric", "safeint"], + ["numeric", "uint8"], + ["numeric", "uint16"], + ["numeric", "uint32"], + ["numeric", "uint64"], + + // uint8 + ["uint8", "int8"], + ])("%s ⇏ %s", async (a, b) => { + const { diagnostics, pos } = await diagnoseUsage(` + const a = ${b}(┆${a}(123)); + `); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: `Type '${a}' is not assignable to type '${b}'`, + pos, + }); + }); + }); + + describe("custom numeric scalars", () => { + it("instantiates a custom scalar", async () => { + const value = await compileValueType(`int4(2)`, "scalar int4 extends integer;"); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.scalar?.name, "int4"); + strictEqual(value.value.asNumber(), 2); + }); + + it("validate value is valid using @minValue and @maxValue", async () => { + const value = await compileValueType( + `int4(2)`, + `@minValue(0) @maxValue(15) scalar uint4 extends integer;` + ); + ok(false); // TODO: implement + }); + }); +}); From 218e454c3d22b9b7c5d7690cae6d2b75a09a3907 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 10:12:14 -0700 Subject: [PATCH 056/184] Tests --- packages/compiler/src/core/checker.ts | 12 +- packages/compiler/test/checker/values.test.ts | 173 +++++++++++++++--- 2 files changed, 154 insertions(+), 31 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index ad330f2ae0..3916616b45 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3358,7 +3358,7 @@ export function createChecker(program: Program): Checker { if (!checkValueOfType(value, scalar, argNode)) { return undefined; } - return { ...value, scalar } as any; + return { ...value, scalar, type: scalar } as any; } // TODO: should those be called eval? @@ -6474,14 +6474,6 @@ export function createChecker(program: Program): Checker { if (!(target.name in numericRanges)) return false; const [low, high, options] = numericRanges[target.name]; - console.log( - "HJEre", - low.toString(), - high.toString(), - source.toString(), - source.gte(low), - source.lte(high) - ); return source.gte(low) && source.lte(high) && (!options.int || isInt); } @@ -6839,7 +6831,7 @@ function isAnonymous(type: Type) { } const numericRanges: Record = { - int64: [Numeric("-9223372036854775807"), Numeric("9223372036854775808"), { int: true }], + int64: [Numeric("-9223372036854775808"), Numeric("9223372036854775807"), { int: true }], int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true }], int16: [Numeric("-32768"), Numeric("32767"), { int: true }], int8: [Numeric("-128"), Numeric("127"), { int: true }], diff --git a/packages/compiler/test/checker/values.test.ts b/packages/compiler/test/checker/values.test.ts index 464cd8a7dc..d9b1329d4b 100644 --- a/packages/compiler/test/checker/values.test.ts +++ b/packages/compiler/test/checker/values.test.ts @@ -304,36 +304,167 @@ describe("numeric literals", () => { }); describe("validate numeric literal is assignable", () => { - // it.each([ - // // numeric - // ["1234", "int8"], - // ["-12", "uint8"], - // ["1234", "int16"], - // ["-21", "uint16"], - // ["-12", "uint32"], - // ["1234", "int32"], - // ["-12", "uint64"], - // ["1234", "int64"], - // ])("%s ⇏ %s", async (a, b) => { - // const { diagnostics, pos } = await diagnoseUsage(` - // const a = ${b}(┆${a}); - // `); - // expectDiagnostics(diagnostics, { - // code: "unassignable", - // message: `Type '${a}' is not assignable to type '${b}'`, - // pos, - // }); - // }); - const cases = [ + const cases: Array<[string, Array<["✔" | "✘", string]>]> = [ + // signed integers [ "int8", [ + ["✔", "0"], ["✔", "123"], ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✘", "128"], + ["✘", "-129"], ["✘", "1234"], ["✘", "-1234"], ], ], + [ + "int16", + [ + ["✔", "0"], + ["✔", "31489"], + ["✔", "-31489"], + ["✘", "32768"], + ["✘", "33489"], + ["✘", "-32769"], + ["✘", "-33489"], + ], + ], + [ + "int32", + [ + ["✔", "-2147483648"], + ["✔", "2147483647"], + ["✘", "2147483648"], + ["✘", "-2147483649"], + ], + ], + [ + "int64", + [ + ["✔", "0"], + ["✔", "-9223372036854775808"], + ["✔", "9223372036854775807"], + ["✘", "-9223372036854775809"], + ["✘", "9223372036854775808"], + ], + ], + [ + "integer", + [ + ["✔", "0"], + ["✔", "-9223372036854775808"], + ["✔", "9223372036854775807"], + ["✔", "-9223372036854775809"], + ["✔", "9223372036854775808"], + ], + ], + // unsigned integers + [ + "uint8", + [ + ["✔", "0"], + ["✔", "128"], + ["✔", "255"], + ["✘", "256"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + [ + "uint16", + [ + ["✔", "0"], + ["✔", "65535"], + ["✘", "65536"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + [ + "uint32", + [ + ["✔", "0"], + ["✔", "4294967295"], + ["✘", "42949672956"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + [ + "uint64", + [ + ["✔", "0"], + ["✔", "18446744073709551615"], + ["✘", "18446744073709551616"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + // floats + [ + "float32", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✘", "3.4e40"], + ["✘", "-3.4e40"], + ], + ], + [ + "float64", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✘", "3.4e309"], + ["✘", "-3.4e309"], + ], + ], + [ + "float", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✔", "3.4e309"], + ["✔", "-3.4e309"], + ], + ], + // decimal + [ + "decimal128", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✔", "3.4e309"], + ["✔", "-3.4e309"], + ], + ], + [ + "decimal", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✔", "3.4e309"], + ["✔", "-3.4e309"], + ], + ], ] as const; describe.each(cases)("%s", (scalarName, perScalarCases) => { it.each(perScalarCases)("%s %s", async (pass, literal) => { From 0503b61bec55cdff6362878f5db49e8b62cff102 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 10:29:24 -0700 Subject: [PATCH 057/184] Fix up tests --- packages/compiler/test/checker/values.test.ts | 580 ------------------ .../test/checker/values/numeric-value.test.ts | 314 ++++++++++ .../compiler/test/checker/values/utils.ts | 59 ++ .../test/checker/values/values.test.ts | 220 +++++++ 4 files changed, 593 insertions(+), 580 deletions(-) delete mode 100644 packages/compiler/test/checker/values.test.ts create mode 100644 packages/compiler/test/checker/values/numeric-value.test.ts create mode 100644 packages/compiler/test/checker/values/utils.ts create mode 100644 packages/compiler/test/checker/values/values.test.ts diff --git a/packages/compiler/test/checker/values.test.ts b/packages/compiler/test/checker/values.test.ts deleted file mode 100644 index d9b1329d4b..0000000000 --- a/packages/compiler/test/checker/values.test.ts +++ /dev/null @@ -1,580 +0,0 @@ -import { ok, strictEqual } from "assert"; -import { describe, it } from "vitest"; -import { Diagnostic, Type, Value } from "../../src/index.js"; -import { - createTestHost, - createTestRunner, - expectDiagnosticEmpty, - expectDiagnostics, - extractCursor, -} from "../../src/testing/index.js"; - -async function diagnoseUsage( - code: string -): Promise<{ diagnostics: readonly Diagnostic[]; pos: number }> { - const runner = await createTestRunner(); - const { source, pos } = extractCursor(code); - const diagnostics = await runner.diagnose(source); - return { diagnostics, pos }; -} - -async function compileAndDiagnoseValueType( - code: string, - other?: string -): Promise<[Value | undefined, readonly Diagnostic[]]> { - const host = await createTestHost(); - let called: Value | undefined; - host.addJsFile("dec.js", { - $collect: (context: DecoratorContext, target: Type, value: Value) => { - called = value; - }, - }); - host.addTypeSpecFile( - "main.tsp", - ` - import "./dec.js"; - - @collect(${code}) - model Test {} - - ${other ?? ""} - ` - ); - const diagnostics = await host.diagnose("main.tsp"); - return [called, diagnostics]; -} - -async function compileValueType(code: string, other?: string): Promise { - const [called, diagnostics] = await compileAndDiagnoseValueType(code, other); - expectDiagnosticEmpty(diagnostics); - ok(called, "Decorator was not called"); - - return called; -} - -async function diagnoseValueType(code: string, other?: string): Promise { - const [_, diagnostics] = await compileAndDiagnoseValueType(code, other); - return diagnostics; -} - -describe("object literals", () => { - it("no properties", async () => { - const object = await compileValueType(`#{}`); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 0); - }); - - it("single property", async () => { - const object = await compileValueType(`#{name: "John"}`); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 1); - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - }); - - it("multiple property", async () => { - const object = await compileValueType(`#{name: "John", age: 21}`); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 2); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - - const ageProp = object.properties.get("age")?.value; - strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value.asNumber(), 21); - }); - - describe("spreading", () => { - it("add the properties", async () => { - const object = await compileValueType( - `#{...Common, age: 21}`, - `alias Common = #{ name: "John" };` - ); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 2); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - - const ageProp = object.properties.get("age")?.value; - strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value.asNumber(), 21); - }); - - it("override properties defined before if there is a name conflict", async () => { - const object = await compileValueType( - `#{name: "John", age: 21, ...Common, }`, - `alias Common = #{ name: "Common" };` - ); - strictEqual(object.valueKind, "ObjectValue"); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "Common"); - }); - - it("override properties spread before", async () => { - const object = await compileValueType( - `#{...Common, name: "John", age: 21 }`, - `alias Common = #{ name: "John" };` - ); - strictEqual(object.valueKind, "ObjectValue"); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - }); - - it("emit diagnostic is spreading something else than an object literal", async () => { - const diagnostics = await diagnoseValueType( - `#{...Common, age: 21}`, - `alias Common = { name: "John" };` - ); - expectDiagnostics(diagnostics, { - code: "spread-object", - message: "Cannot spread properties of non-object type.", - }); - }); - }); - - describe("valid property types", () => { - it.each([ - ["StringValue", `"John"`], - ["NumericValue", "21"], - ["Boolean", "true"], - ["EnumMember", "Direction.up", "enum Direction { up, down }"], - ["ObjectValue", `#{nested: "foo"}`], - ["ArrayValue", `#["foo"]`], - ])("%s", async (kind, type, other?) => { - const object = await compileValueType(`#{prop: ${type}}`, other); - strictEqual(object.valueKind, "ObjectValue"); - const nameProp = object.properties.get("prop")?.value; - strictEqual(nameProp?.valueKind, kind); - }); - }); - - it("emit diagnostic if referencing a non literal type", async () => { - const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); - expectDiagnostics(diagnostics, { - code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", - }); - }); - - describe("emit diagnostic when used in", () => { - it("emit diagnostic when used in a model", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test { - prop: ┆#{ name: "John" }; - } - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - - it("emit diagnostic when used in template constraint", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test {} - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - }); -}); - -describe("tuple literals", () => { - it("no values", async () => { - const object = await compileValueType(`#[]`); - strictEqual(object.valueKind, "ArrayValue"); - strictEqual(object.values.length, 0); - }); - - it("single value", async () => { - const object = await compileValueType(`#["John"]`); - strictEqual(object.valueKind, "ArrayValue"); - strictEqual(object.values.length, 1); - const first = object.values[0]; - strictEqual(first.valueKind, "StringValue"); - strictEqual(first.value, "John"); - }); - - it("multiple property", async () => { - const object = await compileValueType(`#["John", 21]`); - strictEqual(object.valueKind, "ArrayValue"); - strictEqual(object.values.length, 2); - - const nameProp = object.values[0]; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - - const ageProp = object.values[1]; - strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value, 21); - }); - - describe("valid property types", () => { - it.each([ - ["StringValue", `"John"`], - ["NumericValue", "21"], - ["Boolean", "true"], - ["EnumMember", "Direction.up", "enum Direction { up, down }"], - ["ObjectValue", `#{nested: "foo"}`], - ["ArrayValue", `#["foo"]`], - ])("%s", async (kind, type, other?) => { - const object = await compileValueType(`#[${type}]`, other); - strictEqual(object.valueKind, "ArrayValue"); - const nameProp = object.values[0]; - strictEqual(nameProp?.valueKind, kind); - }); - }); - - it("emit diagnostic if referencing a non literal type", async () => { - const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); - expectDiagnostics(diagnostics, { - code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", - }); - }); - - describe("emit diagnostic when used in", () => { - it("emit diagnostic when used in a model", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test { - prop: ┆#["John"]; - } - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - - it("emit diagnostic when used in template constraint", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test {} - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - }); -}); - -describe("numeric literals", () => { - describe("instantiate from numeric literal", () => { - it.each([ - "numeric", - // Integers - "integer", - "int8", - "int16", - "int32", - "int64", - "safeint", - "uint8", - "uint16", - "uint32", - "uint64", - // Floats - "float", - "float32", - "float64", - // Decimals - "decimal", - "decimal128", - ])("%s", async (scalarName) => { - const value = await compileValueType(`${scalarName}(123)`); - strictEqual(value.valueKind, "NumericValue"); - strictEqual(value.scalar?.name, scalarName); - strictEqual(value.value.asNumber(), 123); - }); - }); - - describe("validate numeric literal is assignable", () => { - const cases: Array<[string, Array<["✔" | "✘", string]>]> = [ - // signed integers - [ - "int8", - [ - ["✔", "0"], - ["✔", "123"], - ["✔", "-123"], - ["✔", "127"], - ["✔", "-128"], - ["✘", "128"], - ["✘", "-129"], - ["✘", "1234"], - ["✘", "-1234"], - ], - ], - [ - "int16", - [ - ["✔", "0"], - ["✔", "31489"], - ["✔", "-31489"], - ["✘", "32768"], - ["✘", "33489"], - ["✘", "-32769"], - ["✘", "-33489"], - ], - ], - [ - "int32", - [ - ["✔", "-2147483648"], - ["✔", "2147483647"], - ["✘", "2147483648"], - ["✘", "-2147483649"], - ], - ], - [ - "int64", - [ - ["✔", "0"], - ["✔", "-9223372036854775808"], - ["✔", "9223372036854775807"], - ["✘", "-9223372036854775809"], - ["✘", "9223372036854775808"], - ], - ], - [ - "integer", - [ - ["✔", "0"], - ["✔", "-9223372036854775808"], - ["✔", "9223372036854775807"], - ["✔", "-9223372036854775809"], - ["✔", "9223372036854775808"], - ], - ], - // unsigned integers - [ - "uint8", - [ - ["✔", "0"], - ["✔", "128"], - ["✔", "255"], - ["✘", "256"], - ["✘", "-0"], - ["✘", "-1"], - ], - ], - [ - "uint16", - [ - ["✔", "0"], - ["✔", "65535"], - ["✘", "65536"], - ["✘", "-0"], - ["✘", "-1"], - ], - ], - [ - "uint32", - [ - ["✔", "0"], - ["✔", "4294967295"], - ["✘", "42949672956"], - ["✘", "-0"], - ["✘", "-1"], - ], - ], - [ - "uint64", - [ - ["✔", "0"], - ["✔", "18446744073709551615"], - ["✘", "18446744073709551616"], - ["✘", "-0"], - ["✘", "-1"], - ], - ], - // floats - [ - "float32", - [ - ["✔", "0"], - ["✔", "123"], - ["✔", "-123"], - ["✔", "127"], - ["✔", "-128"], - ["✘", "3.4e40"], - ["✘", "-3.4e40"], - ], - ], - [ - "float64", - [ - ["✔", "0"], - ["✔", "123"], - ["✔", "-123"], - ["✔", "127"], - ["✔", "-128"], - ["✘", "3.4e309"], - ["✘", "-3.4e309"], - ], - ], - [ - "float", - [ - ["✔", "0"], - ["✔", "123"], - ["✔", "-123"], - ["✔", "127"], - ["✔", "-128"], - ["✔", "3.4e309"], - ["✔", "-3.4e309"], - ], - ], - // decimal - [ - "decimal128", - [ - ["✔", "0"], - ["✔", "123"], - ["✔", "-123"], - ["✔", "127"], - ["✔", "-128"], - ["✔", "3.4e309"], - ["✔", "-3.4e309"], - ], - ], - [ - "decimal", - [ - ["✔", "0"], - ["✔", "123"], - ["✔", "-123"], - ["✔", "127"], - ["✔", "-128"], - ["✔", "3.4e309"], - ["✔", "-3.4e309"], - ], - ], - ] as const; - describe.each(cases)("%s", (scalarName, perScalarCases) => { - it.each(perScalarCases)("%s %s", async (pass, literal) => { - const { diagnostics, pos } = await diagnoseUsage(` - const a = ${scalarName}(┆${literal}); - `); - if (pass === "✔") { - expectDiagnosticEmpty(diagnostics); - } else { - expectDiagnostics(diagnostics, { - code: "unassignable", - message: `Type '${literal}' is not assignable to type '${scalarName}'`, - pos, - }); - } - }); - }); - }); - - describe("instantiate from another smaller numeric type", () => { - it.each([ - // int8 - ["int8", "int8"], - ["int8", "int16"], - ["int8", "int32"], - ["int8", "int64"], - ["int8", "integer"], - ["int8", "numeric"], - // uint8 - ["uint8", "int16"], - ["uint8", "int32"], - ["uint8", "int64"], - ["uint8", "integer"], - ["uint8", "numeric"], - // int32 - ["int32", "int32"], - ["int32", "int64"], - ["int32", "integer"], - ["int32", "numeric"], - // uint32 - ["uint32", "int64"], - ["uint32", "integer"], - ["uint32", "numeric"], - ])("%s → %s", async (a, b) => { - const value = await compileValueType(`${b}(${a}(123))`); - strictEqual(value.valueKind, "NumericValue"); - strictEqual(value.scalar?.name, b); - strictEqual(value.value.asNumber(), 123); - }); - }); - - describe("cannot instantiate from a larger numeric type", () => { - it.each([ - // numeric - ["numeric", "integer"], - ["numeric", "int8"], - ["numeric", "int16"], - ["numeric", "int32"], - ["numeric", "int64"], - ["numeric", "safeint"], - ["numeric", "uint8"], - ["numeric", "uint16"], - ["numeric", "uint32"], - ["numeric", "uint64"], - ["numeric", "float"], - ["numeric", "float32"], - ["numeric", "float64"], - ["numeric", "decimal"], - ["numeric", "decimal128"], - - // float32 - ["float32", "integer"], - ["numeric", "int8"], - ["numeric", "int16"], - ["numeric", "int32"], - ["numeric", "int64"], - ["numeric", "safeint"], - ["numeric", "uint8"], - ["numeric", "uint16"], - ["numeric", "uint32"], - ["numeric", "uint64"], - - // uint8 - ["uint8", "int8"], - ])("%s ⇏ %s", async (a, b) => { - const { diagnostics, pos } = await diagnoseUsage(` - const a = ${b}(┆${a}(123)); - `); - expectDiagnostics(diagnostics, { - code: "unassignable", - message: `Type '${a}' is not assignable to type '${b}'`, - pos, - }); - }); - }); - - describe("custom numeric scalars", () => { - it("instantiates a custom scalar", async () => { - const value = await compileValueType(`int4(2)`, "scalar int4 extends integer;"); - strictEqual(value.valueKind, "NumericValue"); - strictEqual(value.scalar?.name, "int4"); - strictEqual(value.value.asNumber(), 2); - }); - - it("validate value is valid using @minValue and @maxValue", async () => { - const value = await compileValueType( - `int4(2)`, - `@minValue(0) @maxValue(15) scalar uint4 extends integer;` - ); - ok(false); // TODO: implement - }); - }); -}); diff --git a/packages/compiler/test/checker/values/numeric-value.test.ts b/packages/compiler/test/checker/values/numeric-value.test.ts new file mode 100644 index 0000000000..14ef1834ce --- /dev/null +++ b/packages/compiler/test/checker/values/numeric-value.test.ts @@ -0,0 +1,314 @@ +import { ok, strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValueType, diagnoseUsage } from "./utils.js"; + +describe("instantiate from numeric literal", () => { + it.each([ + "numeric", + // Integers + "integer", + "int8", + "int16", + "int32", + "int64", + "safeint", + "uint8", + "uint16", + "uint32", + "uint64", + // Floats + "float", + "float32", + "float64", + // Decimals + "decimal", + "decimal128", + ])("%s", async (scalarName) => { + const value = await compileValueType(`${scalarName}(123)`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, scalarName); + strictEqual(value.scalar?.name, scalarName); + strictEqual(value.value.asNumber(), 123); + }); +}); + +describe("validate numeric literal is assignable", () => { + const cases: Array<[string, Array<["✔" | "✘", string]>]> = [ + // signed integers + [ + "int8", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✘", "128"], + ["✘", "-129"], + ["✘", "1234"], + ["✘", "-1234"], + ], + ], + [ + "int16", + [ + ["✔", "0"], + ["✔", "31489"], + ["✔", "-31489"], + ["✘", "32768"], + ["✘", "33489"], + ["✘", "-32769"], + ["✘", "-33489"], + ], + ], + [ + "int32", + [ + ["✔", "-2147483648"], + ["✔", "2147483647"], + ["✘", "2147483648"], + ["✘", "-2147483649"], + ], + ], + [ + "int64", + [ + ["✔", "0"], + ["✔", "-9223372036854775808"], + ["✔", "9223372036854775807"], + ["✘", "-9223372036854775809"], + ["✘", "9223372036854775808"], + ], + ], + [ + "integer", + [ + ["✔", "0"], + ["✔", "-9223372036854775808"], + ["✔", "9223372036854775807"], + ["✔", "-9223372036854775809"], + ["✔", "9223372036854775808"], + ], + ], + // unsigned integers + [ + "uint8", + [ + ["✔", "0"], + ["✔", "128"], + ["✔", "255"], + ["✘", "256"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + [ + "uint16", + [ + ["✔", "0"], + ["✔", "65535"], + ["✘", "65536"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + [ + "uint32", + [ + ["✔", "0"], + ["✔", "4294967295"], + ["✘", "42949672956"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + [ + "uint64", + [ + ["✔", "0"], + ["✔", "18446744073709551615"], + ["✘", "18446744073709551616"], + ["✘", "-0"], + ["✘", "-1"], + ], + ], + // floats + [ + "float32", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✘", "3.4e40"], + ["✘", "-3.4e40"], + ], + ], + [ + "float64", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✘", "3.4e309"], + ["✘", "-3.4e309"], + ], + ], + [ + "float", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✔", "3.4e309"], + ["✔", "-3.4e309"], + ], + ], + // decimal + [ + "decimal128", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✔", "3.4e309"], + ["✔", "-3.4e309"], + ], + ], + [ + "decimal", + [ + ["✔", "0"], + ["✔", "123"], + ["✔", "-123"], + ["✔", "127"], + ["✔", "-128"], + ["✔", "3.4e309"], + ["✔", "-3.4e309"], + ], + ], + ] as const; + describe.each(cases)("%s", (scalarName, perScalarCases) => { + it.each(perScalarCases)("%s %s", async (pass, literal) => { + const { diagnostics, pos } = await diagnoseUsage(` + const a = ${scalarName}(┆${literal}); + `); + if (pass === "✔") { + expectDiagnosticEmpty(diagnostics); + } else { + expectDiagnostics(diagnostics, { + code: "unassignable", + message: `Type '${literal}' is not assignable to type '${scalarName}'`, + pos, + }); + } + }); + }); +}); + +describe("instantiate from another smaller numeric type", () => { + it.each([ + // int8 + ["int8", "int8"], + ["int8", "int16"], + ["int8", "int32"], + ["int8", "int64"], + ["int8", "integer"], + ["int8", "numeric"], + // uint8 + // ["uint8", "int16"], + // ["uint8", "int32"], + // ["uint8", "int64"], + ["uint8", "integer"], + ["uint8", "numeric"], + // int32 + ["int32", "int32"], + ["int32", "int64"], + ["int32", "integer"], + ["int32", "numeric"], + // uint32 + // ["uint32", "int64"], + ["uint32", "integer"], + ["uint32", "numeric"], + ])("%s → %s", async (a, b) => { + const value = await compileValueType(`${b}(${a}(123))`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.scalar?.name, b); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, b); + strictEqual(value.value.asNumber(), 123); + }); +}); + +describe("cannot instantiate from a larger numeric type", () => { + it.each([ + // numeric + ["numeric", "integer"], + ["numeric", "int8"], + ["numeric", "int16"], + ["numeric", "int32"], + ["numeric", "int64"], + ["numeric", "safeint"], + ["numeric", "uint8"], + ["numeric", "uint16"], + ["numeric", "uint32"], + ["numeric", "uint64"], + ["numeric", "float"], + ["numeric", "float32"], + ["numeric", "float64"], + ["numeric", "decimal"], + ["numeric", "decimal128"], + + // float32 + ["float32", "integer"], + ["numeric", "int8"], + ["numeric", "int16"], + ["numeric", "int32"], + ["numeric", "int64"], + ["numeric", "safeint"], + ["numeric", "uint8"], + ["numeric", "uint16"], + ["numeric", "uint32"], + ["numeric", "uint64"], + + // uint8 + ["uint8", "int8"], + ])("%s ⇏ %s", async (a, b) => { + const { diagnostics, pos } = await diagnoseUsage(` + const a = ${b}(┆${a}(123)); + `); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: `Type '${a}' is not assignable to type '${b}'`, + pos, + }); + }); +}); + +describe("custom numeric scalars", () => { + it("instantiates a custom scalar", async () => { + const value = await compileValueType(`int4(2)`, "scalar int4 extends integer;"); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "int4"); + strictEqual(value.scalar?.name, "int4"); + strictEqual(value.value.asNumber(), 2); + }); + + it("validate value is valid using @minValue and @maxValue", async () => { + const value = await compileValueType( + `int4(2)`, + `@minValue(0) @maxValue(15) scalar uint4 extends integer;` + ); + ok(false); // TODO: implement + }); +}); diff --git a/packages/compiler/test/checker/values/utils.ts b/packages/compiler/test/checker/values/utils.ts new file mode 100644 index 0000000000..8d798b691d --- /dev/null +++ b/packages/compiler/test/checker/values/utils.ts @@ -0,0 +1,59 @@ +import { ok } from "assert"; +import { Diagnostic, Type, Value } from "../../../src/index.js"; +import { + createTestHost, + createTestRunner, + expectDiagnosticEmpty, + extractCursor, +} from "../../../src/testing/index.js"; + +export async function diagnoseUsage( + code: string +): Promise<{ diagnostics: readonly Diagnostic[]; pos: number }> { + const runner = await createTestRunner(); + const { source, pos } = extractCursor(code); + const diagnostics = await runner.diagnose(source); + return { diagnostics, pos }; +} + +export async function compileAndDiagnoseValueType( + code: string, + other?: string +): Promise<[Value | undefined, readonly Diagnostic[]]> { + const host = await createTestHost(); + let called: Value | undefined; + host.addJsFile("dec.js", { + $collect: (context: DecoratorContext, target: Type, value: Value) => { + called = value; + }, + }); + host.addTypeSpecFile( + "main.tsp", + ` + import "./dec.js"; + + @collect(${code}) + model Test {} + + ${other ?? ""} + ` + ); + const diagnostics = await host.diagnose("main.tsp"); + return [called, diagnostics]; +} + +export async function compileValueType(code: string, other?: string): Promise { + const [called, diagnostics] = await compileAndDiagnoseValueType(code, other); + expectDiagnosticEmpty(diagnostics); + ok(called, "Decorator was not called"); + + return called; +} + +export async function diagnoseValueType( + code: string, + other?: string +): Promise { + const [_, diagnostics] = await compileAndDiagnoseValueType(code, other); + return diagnostics; +} diff --git a/packages/compiler/test/checker/values/values.test.ts b/packages/compiler/test/checker/values/values.test.ts new file mode 100644 index 0000000000..59872ab43c --- /dev/null +++ b/packages/compiler/test/checker/values/values.test.ts @@ -0,0 +1,220 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/index.js"; +import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; + +describe("object literals", () => { + it("no properties", async () => { + const object = await compileValueType(`#{}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 0); + }); + + it("single property", async () => { + const object = await compileValueType(`#{name: "John"}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 1); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + }); + + it("multiple property", async () => { + const object = await compileValueType(`#{name: "John", age: 21}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 2); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.properties.get("age")?.value; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value.asNumber(), 21); + }); + + describe("spreading", () => { + it("add the properties", async () => { + const object = await compileValueType( + `#{...Common, age: 21}`, + `alias Common = #{ name: "John" };` + ); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 2); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.properties.get("age")?.value; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value.asNumber(), 21); + }); + + it("override properties defined before if there is a name conflict", async () => { + const object = await compileValueType( + `#{name: "John", age: 21, ...Common, }`, + `alias Common = #{ name: "Common" };` + ); + strictEqual(object.valueKind, "ObjectValue"); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "Common"); + }); + + it("override properties spread before", async () => { + const object = await compileValueType( + `#{...Common, name: "John", age: 21 }`, + `alias Common = #{ name: "John" };` + ); + strictEqual(object.valueKind, "ObjectValue"); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + }); + + it("emit diagnostic is spreading something else than an object literal", async () => { + const diagnostics = await diagnoseValueType( + `#{...Common, age: 21}`, + `alias Common = { name: "John" };` + ); + expectDiagnostics(diagnostics, { + code: "spread-object", + message: "Cannot spread properties of non-object type.", + }); + }); + }); + + describe("valid property types", () => { + it.each([ + ["StringValue", `"John"`], + ["NumericValue", "21"], + ["Boolean", "true"], + ["EnumMember", "Direction.up", "enum Direction { up, down }"], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], + ])("%s", async (kind, type, other?) => { + const object = await compileValueType(`#{prop: ${type}}`, other); + strictEqual(object.valueKind, "ObjectValue"); + const nameProp = object.properties.get("prop")?.value; + strictEqual(nameProp?.valueKind, kind); + }); + }); + + it("emit diagnostic if referencing a non literal type", async () => { + const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "(anonymous model) refers to a type, but is being used as a value here.", + }); + }); + + describe("emit diagnostic when used in", () => { + it("emit diagnostic when used in a model", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test { + prop: ┆#{ name: "John" }; + } + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + + it("emit diagnostic when used in template constraint", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + }); +}); + +describe("tuple literals", () => { + it("no values", async () => { + const object = await compileValueType(`#[]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 0); + }); + + it("single value", async () => { + const object = await compileValueType(`#["John"]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 1); + const first = object.values[0]; + strictEqual(first.valueKind, "StringValue"); + strictEqual(first.value, "John"); + }); + + it("multiple property", async () => { + const object = await compileValueType(`#["John", 21]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 2); + + const nameProp = object.values[0]; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.values[1]; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value, 21); + }); + + describe("valid property types", () => { + it.each([ + ["StringValue", `"John"`], + ["NumericValue", "21"], + ["Boolean", "true"], + ["EnumMember", "Direction.up", "enum Direction { up, down }"], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], + ])("%s", async (kind, type, other?) => { + const object = await compileValueType(`#[${type}]`, other); + strictEqual(object.valueKind, "ArrayValue"); + const nameProp = object.values[0]; + strictEqual(nameProp?.valueKind, kind); + }); + }); + + it("emit diagnostic if referencing a non literal type", async () => { + const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "(anonymous model) refers to a type, but is being used as a value here.", + }); + }); + + describe("emit diagnostic when used in", () => { + it("emit diagnostic when used in a model", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test { + prop: ┆#["John"]; + } + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + + it("emit diagnostic when used in template constraint", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + }); +}); From 2bc3ee410617f71f9a6daec5ae8032a1da8ca417 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 11:29:18 -0700 Subject: [PATCH 058/184] infer type from const value --- packages/compiler/src/core/checker.ts | 71 ++++++++++++++++--- packages/compiler/src/core/messages.ts | 6 ++ ...c-value.test.ts => numeric-values.test.ts} | 59 +++++++++------ 3 files changed, 103 insertions(+), 33 deletions(-) rename packages/compiler/test/checker/values/{numeric-value.test.ts => numeric-values.test.ts} (88%) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3916616b45..d277c4f59f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -690,11 +690,11 @@ export function createChecker(program: Program): Checker { case SyntaxKind.ConstStatement: return checkConst(node); case SyntaxKind.StringLiteral: - return checkStringValue(node, undefined); + return checkStringValue(node, constraint); case SyntaxKind.NumericLiteral: - return checkNumericValue(node, undefined); + return checkNumericValue(node, constraint); case SyntaxKind.BooleanLiteral: - return checkBooleanValue(node, undefined); + return checkBooleanValue(node, constraint); case SyntaxKind.TypeReference: return checkValueReference(node, mapper); case SyntaxKind.CallExpression: @@ -744,15 +744,15 @@ export function createChecker(program: Program): Checker { return checkOperation(node, mapper); case SyntaxKind.NumericLiteral: return constraint?.kind === "Value" - ? checkNumericValue(node, undefined) + ? checkNumericValue(node, constraint.target) : checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: return constraint?.kind === "Value" - ? checkBooleanValue(node, undefined) + ? checkBooleanValue(node, constraint.target) : checkBooleanLiteral(node); case SyntaxKind.StringLiteral: return constraint?.kind === "Value" - ? checkStringValue(node, undefined) + ? checkStringValue(node, constraint.target) : checkStringLiteral(node); case SyntaxKind.TupleExpression: return checkTupleExpression(node, mapper); @@ -3258,7 +3258,56 @@ export function createChecker(program: Program): Checker { }; } - function checkStringValue(node: StringLiteralNode, scalar: Scalar | undefined): StringValue { + function inferScalarForPrimitiveValue( + base: Scalar, + type: Type | undefined, + node: StringLiteralNode | NumericLiteralNode | BooleanLiteralNode + ): Scalar | undefined { + if (type === undefined) { + return undefined; + } + switch (type.kind) { + case "Scalar": + if (areScalarsRelated(type, base)) { + return type; + } + return undefined; // TODO: do i need to report an error here + case "Union": + let found = undefined; + for (const variant of type.variants.values()) { + const scalar = inferScalarForPrimitiveValue(base, variant.type, node); + if (scalar) { + if (found) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "ambiguous-scalar-type", + format: { + value: + node.kind === SyntaxKind.StringLiteral + ? `"${node.value}"` + : node.kind === SyntaxKind.NumericLiteral + ? node.valueAsString + : node.value.toString(), + types: [found, scalar].map((x) => x.name).join(", "), + example: found.name, + }, + target: node, + }) + ); + return undefined; + } else { + found = scalar; + } + } + } + return found; + default: + return undefined; + } + } + + function checkStringValue(node: StringLiteralNode, type: Type | undefined): StringValue { + const scalar = inferScalarForPrimitiveValue(getStdType("string"), type, node); return { valueKind: "StringValue", value: node.value, @@ -3267,7 +3316,8 @@ export function createChecker(program: Program): Checker { }; } - function checkNumericValue(node: NumericLiteralNode, scalar: Scalar | undefined): NumericValue { + function checkNumericValue(node: NumericLiteralNode, type: Type | undefined): NumericValue { + const scalar = inferScalarForPrimitiveValue(getStdType("numeric"), type, node); return { valueKind: "NumericValue", value: Numeric(node.valueAsString), @@ -3276,7 +3326,8 @@ export function createChecker(program: Program): Checker { }; } - function checkBooleanValue(node: BooleanLiteralNode, scalar: Scalar | undefined): BooleanValue { + function checkBooleanValue(node: BooleanLiteralNode, type: Type | undefined): BooleanValue { + const scalar = inferScalarForPrimitiveValue(getStdType("boolean"), type, node); return { valueKind: "BooleanValue", value: node.value, @@ -4561,7 +4612,7 @@ export function createChecker(program: Program): Checker { } pendingResolutions.start(symId, ResolutionKind.Value); - const value = getValueForNode(node.value, undefined); + const value = getValueForNode(node.value, undefined, type); pendingResolutions.finish(symId, ResolutionKind.Value); if (value === undefined) { return undefined; diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5c8ef708a4..2caa295465 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -429,6 +429,12 @@ const diagnostics = { invalidArg: paramMessage`Expected a single argument of type ${"expected"} but got ${"actual"}.`, }, }, + "ambiguous-scalar-type": { + severity: "error", + messages: { + default: paramMessage`Value ${"value"} type is ambiguous between ${"types"}. To resolve be explicit when instantiating this value(e.g. ${"example"}(${"value"})).`, + }, + }, unassignable: { severity: "error", messages: { diff --git a/packages/compiler/test/checker/values/numeric-value.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts similarity index 88% rename from packages/compiler/test/checker/values/numeric-value.test.ts rename to packages/compiler/test/checker/values/numeric-values.test.ts index 14ef1834ce..6a441be558 100644 --- a/packages/compiler/test/checker/values/numeric-value.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -3,28 +3,30 @@ import { describe, it } from "vitest"; import { expectDiagnosticEmpty, expectDiagnostics } from "../../../src/testing/expect.js"; import { compileValueType, diagnoseUsage } from "./utils.js"; -describe("instantiate from numeric literal", () => { - it.each([ - "numeric", - // Integers - "integer", - "int8", - "int16", - "int32", - "int64", - "safeint", - "uint8", - "uint16", - "uint32", - "uint64", - // Floats - "float", - "float32", - "float64", - // Decimals - "decimal", - "decimal128", - ])("%s", async (scalarName) => { +const numericScalars = [ + "numeric", + // Integers + "integer", + "int8", + "int16", + "int32", + "int64", + "safeint", + "uint8", + "uint16", + "uint32", + "uint64", + // Floats + "float", + "float32", + "float64", + // Decimals + "decimal", + "decimal128", +]; + +describe("instantiate with constructor", () => { + it.each(numericScalars)("%s", async (scalarName) => { const value = await compileValueType(`${scalarName}(123)`); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.type.kind, "Scalar"); @@ -34,6 +36,17 @@ describe("instantiate from numeric literal", () => { }); }); +describe("instantiate from implicit const type", () => { + it.each(numericScalars)("%s", async (scalarName) => { + const value = await compileValueType(`a`, `const a:${scalarName} = 123;`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, scalarName); + strictEqual(value.scalar?.name, scalarName); + strictEqual(value.value.asNumber(), 123); + }); +}); + describe("validate numeric literal is assignable", () => { const cases: Array<[string, Array<["✔" | "✘", string]>]> = [ // signed integers @@ -225,7 +238,7 @@ describe("instantiate from another smaller numeric type", () => { ["int8", "integer"], ["int8", "numeric"], // uint8 - // ["uint8", "int16"], + // ["uint8", "int16"], https://github.com/microsoft/typespec/issues/3156 // ["uint8", "int32"], // ["uint8", "int64"], ["uint8", "integer"], From 9f2898c2f31d02f101a034038a01b91b3895ba07 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 11:32:59 -0700 Subject: [PATCH 059/184] add test for ambiguous numeric --- packages/compiler/src/core/messages.ts | 2 +- .../checker/values/numeric-values.test.ts | 33 +++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 2caa295465..d37a69c09d 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -432,7 +432,7 @@ const diagnostics = { "ambiguous-scalar-type": { severity: "error", messages: { - default: paramMessage`Value ${"value"} type is ambiguous between ${"types"}. To resolve be explicit when instantiating this value(e.g. ${"example"}(${"value"})).`, + default: paramMessage`Value ${"value"} type is ambiguous between ${"types"}. To resolve be explicit when instantiating this value(e.g. '${"example"}(${"value"})').`, }, }, unassignable: { diff --git a/packages/compiler/test/checker/values/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts index 6a441be558..6e9844fde5 100644 --- a/packages/compiler/test/checker/values/numeric-values.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -1,7 +1,7 @@ import { ok, strictEqual } from "assert"; import { describe, it } from "vitest"; import { expectDiagnosticEmpty, expectDiagnostics } from "../../../src/testing/expect.js"; -import { compileValueType, diagnoseUsage } from "./utils.js"; +import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; const numericScalars = [ "numeric", @@ -36,15 +36,34 @@ describe("instantiate with constructor", () => { }); }); -describe("instantiate from implicit const type", () => { - it.each(numericScalars)("%s", async (scalarName) => { - const value = await compileValueType(`a`, `const a:${scalarName} = 123;`); +describe("implicit type", () => { + describe("instantiate when type is scalar", () => { + it.each(numericScalars)("%s", async (scalarName) => { + const value = await compileValueType(`a`, `const a:${scalarName} = 123;`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, scalarName); + strictEqual(value.scalar?.name, scalarName); + strictEqual(value.value.asNumber(), 123); + }); + }); + + it("instantiate if there is a single numeric option", async () => { + const value = await compileValueType(`a`, `const a: int32 | string = 123;`); strictEqual(value.valueKind, "NumericValue"); - strictEqual(value.type.kind, "Scalar"); - strictEqual(value.type.name, scalarName); - strictEqual(value.scalar?.name, scalarName); + strictEqual(value.type.kind, "Union"); + strictEqual(value.scalar?.name, "int32"); strictEqual(value.value.asNumber(), 123); }); + + it("emit diagnostics if there is multiple numeric choices", async () => { + const diagnostics = await diagnoseValueType(`a`, `const a: int32 | int64 = 123;`); + expectDiagnostics(diagnostics, { + code: "ambiguous-scalar-type", + message: + "Value 123 type is ambiguous between int32, int64. To resolve be explicit when instantiating this value(e.g. 'int32(123)')", + }); + }); }); describe("validate numeric literal is assignable", () => { From a964e9d1655b04cfe9f2cf2e1aac3f015525c6ab Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 12:12:41 -0700 Subject: [PATCH 060/184] cache constant values --- packages/compiler/src/core/checker.ts | 8 +++++++- packages/compiler/src/core/types.ts | 7 +++++-- .../compiler/test/checker/values/numeric-values.test.ts | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d277c4f59f..39ac194d9b 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4597,6 +4597,11 @@ export function createChecker(program: Program): Checker { } function checkConst(node: ConstStatementNode): Value | undefined { + const links = getSymbolLinks(node.symbol); + if (links.value) { + return links.value; + } + const type = node.type ? getTypeForNode(node.type, undefined) : undefined; const symId = getSymbolId(node.symbol); @@ -4617,7 +4622,8 @@ export function createChecker(program: Program): Checker { if (value === undefined) { return undefined; } - return type ? { ...value, type } : { ...value }; + links.value = type ? { ...value, type } : { ...value }; + return links.value; } function checkEnum(node: EnumStatementNode, mapper: TypeMapper | undefined): Type { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index a80272cbe4..631ffc7d76 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -738,10 +738,13 @@ export interface Sym { export interface SymbolLinks { type?: Type; - // for types which can be instantiated, we split `type` into declaredType and - // a map of instantiations. + /** For types that can be instanitated this is the type of the declaration */ declaredType?: Type; + /** For types that can be instanitated those are the types per instantiation */ instantiations?: TypeInstantiationMap; + + /** For const statements the value of the const */ + value?: Value; } /** diff --git a/packages/compiler/test/checker/values/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts index 6e9844fde5..3eee6fd49d 100644 --- a/packages/compiler/test/checker/values/numeric-values.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -61,7 +61,7 @@ describe("implicit type", () => { expectDiagnostics(diagnostics, { code: "ambiguous-scalar-type", message: - "Value 123 type is ambiguous between int32, int64. To resolve be explicit when instantiating this value(e.g. 'int32(123)')", + "Value 123 type is ambiguous between int32, int64. To resolve be explicit when instantiating this value(e.g. 'int32(123)').", }); }); }); From 5701b7b6124f34ccfa84095f50852fbcebef77fe Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 13:51:29 -0700 Subject: [PATCH 061/184] Add string and boolean values tests --- packages/compiler/src/core/checker.ts | 121 ++++++++++-------- packages/compiler/src/core/types.ts | 2 +- .../checker/values/boolean-values.test.ts | 68 ++++++++++ .../checker/values/numeric-values.test.ts | 9 ++ .../test/checker/values/string-values.test.ts | 103 +++++++++++++++ 5 files changed, 251 insertions(+), 52 deletions(-) create mode 100644 packages/compiler/test/checker/values/boolean-values.test.ts create mode 100644 packages/compiler/test/checker/values/string-values.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 39ac194d9b..5a7481cc6a 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -19,6 +19,7 @@ import { getEntityName, getNamespaceFullName, getTypeName, + stringTemplateToString, } from "./helpers/index.js"; import { marshallTypeForJSWithLegacyCast, tryMarshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; @@ -681,7 +682,7 @@ export function createChecker(program: Program): Checker { return typeOrValue; } - function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | undefined { + function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | null { switch (node.kind) { case SyntaxKind.ObjectLiteral: return checkObjectLiteral(node, mapper); @@ -690,7 +691,8 @@ export function createChecker(program: Program): Checker { case SyntaxKind.ConstStatement: return checkConst(node); case SyntaxKind.StringLiteral: - return checkStringValue(node, constraint); + case SyntaxKind.StringTemplateExpression: + return checkStringValue(node, mapper, constraint); case SyntaxKind.NumericLiteral: return checkNumericValue(node, constraint); case SyntaxKind.BooleanLiteral: @@ -707,7 +709,7 @@ export function createChecker(program: Program): Checker { target: node, }) ); - return undefined; + return null; } } @@ -752,7 +754,7 @@ export function createChecker(program: Program): Checker { : checkBooleanLiteral(node); case SyntaxKind.StringLiteral: return constraint?.kind === "Value" - ? checkStringValue(node, constraint.target) + ? checkStringValue(node, mapper, constraint.target) : checkStringLiteral(node); case SyntaxKind.TupleExpression: return checkTupleExpression(node, mapper); @@ -1257,7 +1259,7 @@ export function createChecker(program: Program): Checker { sym: Sym, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined - ): Value | undefined { + ): Value | null { // TODO: use common checkTypeOrValueReferenceSymbol if (sym.flags & SymbolFlags.Const) { return getValueForNode(sym.declarations[0], mapper); @@ -1269,7 +1271,7 @@ export function createChecker(program: Program): Checker { target: node, }) ); - return undefined; + return null; } /** * Check and resolve the type for the given symbol + node. @@ -1292,7 +1294,7 @@ export function createChecker(program: Program): Checker { } const result = checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplates); - if (result === undefined || isValue(result)) { + if (result === null || isValue(result)) { reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); return errorType; } @@ -1304,7 +1306,7 @@ export function createChecker(program: Program): Checker { node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplates = true - ): Type | Value | undefined { + ): Type | Value | null { if (sym.flags & SymbolFlags.Const) { return getValueForNode(sym.declarations[0], mapper); } @@ -3181,7 +3183,7 @@ export function createChecker(program: Program): Checker { for (const prop of node.properties!) { if ("id" in prop) { const value = getValueForNode(prop.value, mapper); - if (value !== undefined) { + if (value !== null) { properties.set(prop.id.sv, { name: prop.id.sv, value: value }); } } else { @@ -3226,14 +3228,14 @@ export function createChecker(program: Program): Checker { function checkObjectSpreadProperty( targetNode: TypeReferenceNode, mapper: TypeMapper | undefined - ): ObjectValue | undefined { + ): ObjectValue | null { const value = getValueForNode(targetNode, mapper); - if (value === undefined) { - return undefined; + if (value === null) { + return null; } if (value.valueKind !== "ObjectValue") { reportCheckerDiagnostic(createDiagnostic({ code: "spread-object", target: targetNode })); - return undefined; + return null; } return value; @@ -3261,7 +3263,7 @@ export function createChecker(program: Program): Checker { function inferScalarForPrimitiveValue( base: Scalar, type: Type | undefined, - node: StringLiteralNode | NumericLiteralNode | BooleanLiteralNode + literalType: Type ): Scalar | undefined { if (type === undefined) { return undefined; @@ -3275,23 +3277,18 @@ export function createChecker(program: Program): Checker { case "Union": let found = undefined; for (const variant of type.variants.values()) { - const scalar = inferScalarForPrimitiveValue(base, variant.type, node); + const scalar = inferScalarForPrimitiveValue(base, variant.type, literalType); if (scalar) { if (found) { reportCheckerDiagnostic( createDiagnostic({ code: "ambiguous-scalar-type", format: { - value: - node.kind === SyntaxKind.StringLiteral - ? `"${node.value}"` - : node.kind === SyntaxKind.NumericLiteral - ? node.valueAsString - : node.value.toString(), + value: getTypeName(literalType), types: [found, scalar].map((x) => x.name).join(", "), example: found.name, }, - target: node, + target: literalType, }) ); return undefined; @@ -3306,32 +3303,50 @@ export function createChecker(program: Program): Checker { } } - function checkStringValue(node: StringLiteralNode, type: Type | undefined): StringValue { - const scalar = inferScalarForPrimitiveValue(getStdType("string"), type, node); + function checkStringValue( + node: StringLiteralNode | StringTemplateExpressionNode, + mapper: TypeMapper | undefined, + type: Type | undefined + ): StringValue { + let literalType: StringLiteral | StringTemplate; + let value: string; + if (node.kind === SyntaxKind.StringTemplateExpression) { + literalType = checkStringTemplateExpresion(node, mapper); + const [result, diagnostics] = stringTemplateToString(literalType); + value = result; + reportCheckerDiagnostics(diagnostics); + } else { + literalType = getLiteralType(node); + value = literalType.value; + } + const scalar = inferScalarForPrimitiveValue(getStdType("string"), type, literalType); return { valueKind: "StringValue", - value: node.value, - type: getLiteralType(node), + value, + type: type ?? literalType, scalar, }; } function checkNumericValue(node: NumericLiteralNode, type: Type | undefined): NumericValue { - const scalar = inferScalarForPrimitiveValue(getStdType("numeric"), type, node); + const literalType = getLiteralType(node); + + const scalar = inferScalarForPrimitiveValue(getStdType("numeric"), type, literalType); return { valueKind: "NumericValue", value: Numeric(node.valueAsString), - type: getLiteralType(node), + type: literalType, scalar, }; } function checkBooleanValue(node: BooleanLiteralNode, type: Type | undefined): BooleanValue { - const scalar = inferScalarForPrimitiveValue(getStdType("boolean"), type, node); + const literalType = getLiteralType(node); + const scalar = inferScalarForPrimitiveValue(getStdType("boolean"), type, literalType); return { valueKind: "BooleanValue", value: node.value, - type: getLiteralType(node), + type: type ?? literalType, scalar, }; } @@ -3346,10 +3361,10 @@ export function createChecker(program: Program): Checker { function checkValueReference( node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined - ): Value | undefined { + ): Value | null { const sym = resolveTypeReferenceSym(node, mapper); if (!sym) { - return undefined; + return null; } const value = checkValueReferenceSymbol(sym, node, mapper); @@ -3359,7 +3374,7 @@ export function createChecker(program: Program): Checker { function checkCallExpressionTarget( node: CallExpressionNode, mapper: TypeMapper | undefined - ): ScalarConstructor | Scalar | undefined { + ): ScalarConstructor | Scalar | null { const target = checkTypeReference(node.target, mapper); if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { return target; @@ -3371,7 +3386,7 @@ export function createChecker(program: Program): Checker { target: node.target, }) ); - return undefined; + return null; } } @@ -3380,7 +3395,7 @@ export function createChecker(program: Program): Checker { node: CallExpressionNode, scalar: Scalar, valueKind: T["valueKind"] - ): T | undefined { + ): T | null { if (node.arguments.length !== 1) { reportCheckerDiagnostic( createDiagnostic({ @@ -3388,12 +3403,12 @@ export function createChecker(program: Program): Checker { target: node.target, }) ); - return undefined; + return null; } const argNode = node.arguments[0]; const value = getValueForNode(argNode, undefined); - if (value === undefined) { - return undefined; // error should already have been reported above. + if (value === null) { + return null; // error should already have been reported above. } if (value.valueKind !== valueKind) { reportCheckerDiagnostic( @@ -3404,10 +3419,10 @@ export function createChecker(program: Program): Checker { target: argNode, }) ); - return undefined; + return null; } if (!checkValueOfType(value, scalar, argNode)) { - return undefined; + return null; } return { ...value, scalar, type: scalar } as any; } @@ -3416,18 +3431,21 @@ export function createChecker(program: Program): Checker { function checkCallExpression( node: CallExpressionNode, mapper: TypeMapper | undefined - ): Value | undefined { + ): Value | null { const target = checkCallExpressionTarget(node, mapper); - if (target === undefined) { - return; + if (target === null) { + return null; } if (target.kind === "ScalarConstructor") { - const args = node.arguments.map((x) => getValueForNode(x, mapper)).filter(isDefined); + const args = node.arguments.map((x) => getValueForNode(x, mapper)); + if (args.some((x) => x === null)) { + return null; + } return { valueKind: "ScalarValue", value: { name: target.name, - args, + args: args as Value[], }, scalar: target.scalar, type: target, @@ -3448,7 +3466,7 @@ export function createChecker(program: Program): Checker { target: node.target, }) ); - return undefined; + return null; } } @@ -4596,9 +4614,9 @@ export function createChecker(program: Program): Checker { return type; } - function checkConst(node: ConstStatementNode): Value | undefined { + function checkConst(node: ConstStatementNode): Value | null { const links = getSymbolLinks(node.symbol); - if (links.value) { + if (links.value !== undefined) { return links.value; } @@ -4613,14 +4631,15 @@ export function createChecker(program: Program): Checker { target: node, }) ); - return undefined; + return null; } pendingResolutions.start(symId, ResolutionKind.Value); const value = getValueForNode(node.value, undefined, type); pendingResolutions.finish(symId, ResolutionKind.Value); - if (value === undefined) { - return undefined; + if (value === null || (type && !checkValueOfType(value, type, node.id))) { + links.value = null; + return links.value; } links.value = type ? { ...value, type } : { ...value }; return links.value; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 631ffc7d76..c8043b807a 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -744,7 +744,7 @@ export interface SymbolLinks { instantiations?: TypeInstantiationMap; /** For const statements the value of the const */ - value?: Value; + value?: Value | null; } /** diff --git a/packages/compiler/test/checker/values/boolean-values.test.ts b/packages/compiler/test/checker/values/boolean-values.test.ts new file mode 100644 index 0000000000..24fa3eae1c --- /dev/null +++ b/packages/compiler/test/checker/values/boolean-values.test.ts @@ -0,0 +1,68 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValueType, diagnoseValueType } from "./utils.js"; + +describe("instantiate with constructor", () => { + it("with boolean literal", async () => { + const value = await compileValueType(`boolean(true)`); + strictEqual(value.valueKind, "BooleanValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "boolean"); + strictEqual(value.scalar?.name, "boolean"); + strictEqual(value.value, true); + }); +}); + +describe("implicit type", () => { + it("doesn't pick scalar if const has no type", async () => { + const value = await compileValueType(`a`, `const a = true;`); + strictEqual(value.valueKind, "BooleanValue"); + strictEqual(value.type.kind, "Boolean"); + strictEqual(value.type.value, true); + strictEqual(value.scalar, undefined); + strictEqual(value.value, true); + }); + + it("instantiate if there is a single string option", async () => { + const value = await compileValueType(`a`, `const a: boolean | string = true;`); + strictEqual(value.valueKind, "BooleanValue"); + strictEqual(value.type.kind, "Union"); + strictEqual(value.scalar?.name, "boolean"); + strictEqual(value.value, true); + }); + + it("emit diagnostics if there is multiple numeric choices", async () => { + const diagnostics = await diagnoseValueType( + `a`, + ` + const a: boolean | myBoolean = true; + scalar myBoolean extends boolean;` + ); + expectDiagnostics(diagnostics, { + code: "ambiguous-scalar-type", + message: `Value true type is ambiguous between boolean, myBoolean. To resolve be explicit when instantiating this value(e.g. 'boolean(true)').`, + }); + }); +}); + +describe("validate literal are assignable", () => { + const cases: Array<[string, Array<["✔" | "✘", string, string?]>]> = [ + [ + "boolean", + [ + ["✔", `false`], + ["✔", `true`], + ["✘", `"boolean"`, "Expected a single argument of type BooleanValue but got StringValue."], + ["✘", `123`, "Expected a single argument of type BooleanValue but got NumericValue."], + ], + ], + ]; + + describe.each(cases)("%s", (scalarName, values) => { + it.each(values)(`%s %s`, async (expected, value, message) => { + const diagnostics = await diagnoseValueType(`${scalarName}(${value})`); + expectDiagnostics(diagnostics, expected === "✔" ? [] : [{ message: message ?? "" }]); + }); + }); +}); diff --git a/packages/compiler/test/checker/values/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts index 3eee6fd49d..0e92357063 100644 --- a/packages/compiler/test/checker/values/numeric-values.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -48,6 +48,15 @@ describe("implicit type", () => { }); }); + it("doesn't pick scalar if const has no type", async () => { + const value = await compileValueType(`a`, `const a = 123;`); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.type.kind, "Number"); + strictEqual(value.type.valueAsString, "123"); + strictEqual(value.scalar, undefined); + strictEqual(value.value.asNumber(), 123); + }); + it("instantiate if there is a single numeric option", async () => { const value = await compileValueType(`a`, `const a: int32 | string = 123;`); strictEqual(value.valueKind, "NumericValue"); diff --git a/packages/compiler/test/checker/values/string-values.test.ts b/packages/compiler/test/checker/values/string-values.test.ts new file mode 100644 index 0000000000..307e40f0e3 --- /dev/null +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -0,0 +1,103 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValueType, diagnoseValueType } from "./utils.js"; + +describe("instantiate with constructor", () => { + it("string", async () => { + const value = await compileValueType(`string("abc")`); + strictEqual(value.valueKind, "StringValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "string"); + strictEqual(value.scalar?.name, "string"); + strictEqual(value.value, "abc"); + }); +}); + +describe("implicit type", () => { + it("doesn't pick scalar if const has no type (string literal)", async () => { + const value = await compileValueType(`a`, `const a = "abc";`); + strictEqual(value.valueKind, "StringValue"); + strictEqual(value.type.kind, "String"); + strictEqual(value.type.value, "abc"); + strictEqual(value.scalar, undefined); + strictEqual(value.value, "abc"); + }); + it("doesn't pick scalar if const has no type (string template )", async () => { + const value = await compileValueType(`a`, `const a = "one ${"abc"} def";`); + strictEqual(value.valueKind, "StringValue"); + strictEqual(value.type.kind, "String"); + strictEqual(value.type.value, "one abc def"); + strictEqual(value.scalar, undefined); + strictEqual(value.value, "one abc def"); + }); + + it("instantiate if there is a single string option", async () => { + const value = await compileValueType(`a`, `const a: int32 | string = "abc";`); + strictEqual(value.valueKind, "StringValue"); + strictEqual(value.type.kind, "Union"); + strictEqual(value.scalar?.name, "string"); + strictEqual(value.value, "abc"); + }); + + it("emit diagnostics if there is multiple numeric choices", async () => { + const diagnostics = await diagnoseValueType( + `a`, + ` + const a: string | myString = "abc"; + scalar myString extends string;` + ); + expectDiagnostics(diagnostics, { + code: "ambiguous-scalar-type", + message: `Value "abc" type is ambiguous between string, myString. To resolve be explicit when instantiating this value(e.g. 'string("abc")').`, + }); + }); +}); + +describe("string templates", () => { + it("create string value from string template if able to serialize to string", async () => { + const value = await compileValueType(`string("one \${"abc"} def")`); + strictEqual(value.valueKind, "StringValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "string"); + strictEqual(value.scalar?.name, "string"); + strictEqual(value.value, "one abc def"); + }); + it("emit error if string template is not serializable to string", async () => { + const diagnostics = await diagnoseValueType(`string("one \${boolean} def")`); + expectDiagnostics(diagnostics, { + code: "non-literal-string-template", + message: + "Value interpolated in this string template cannot be converted to a string. Only literal types can be automatically interpolated.", + }); + }); +}); + +describe("validate literal are assignable", () => { + const cases: Array<[string, Array<["✔" | "✘", string, string?]>]> = [ + [ + "string", + [ + ["✔", `""`], + ["✔", `"abc"`], + ["✔", `"one \${"abc"} def"`], + ["✘", `123`, "Type '123' is not assignable to type 'string'"], + ], + ], + [ + `"abc"`, + [ + ["✔", `"abc"`], + ["✔", `"a\${"b"}c"`], + [`✘`, `string("abc")`, `Type 'string' is not assignable to type '"abc"'`], + ], + ], + ]; + + describe.each(cases)("%s", (scalarName, values) => { + it.each(values)(`%s %s`, async (expected, value, message) => { + const diagnostics = await diagnoseValueType(`a`, `const a:${scalarName} = ${value};`); + expectDiagnostics(diagnostics, expected === "✔" ? [] : [{ message: message ?? "" }]); + }); + }); +}); From be288fda00626e98479d2bc8904523ad58ed3828 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 14:55:20 -0700 Subject: [PATCH 062/184] Scalar values --- packages/compiler/src/core/checker.ts | 115 +++++++++-- packages/compiler/src/core/messages.ts | 2 +- packages/compiler/src/core/types.ts | 2 +- .../test/checker/values/scalar-values.test.ts | 183 ++++++++++++++++++ 4 files changed, 286 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/test/checker/values/scalar-values.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5a7481cc6a..470686e4e2 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -137,6 +137,7 @@ import { ScalarConstructor, ScalarConstructorNode, ScalarStatementNode, + ScalarValue, StdTypeName, StdTypes, StringLiteral, @@ -3382,7 +3383,7 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic( createDiagnostic({ code: "non-callable", - format: { typeKind: target.kind }, + format: { type: target.kind }, target: node.target, }) ); @@ -3427,6 +3428,104 @@ export function createChecker(program: Program): Checker { return { ...value, scalar, type: scalar } as any; } + function createScalarValue( + node: CallExpressionNode, + mapper: TypeMapper | undefined, + declaration: ScalarConstructor + ): ScalarValue | null { + let hasError = false; + + const minArgs = declaration.parameters.filter((x) => !x.optional && !x.rest).length ?? 0; + const maxArgs = declaration.parameters[declaration.parameters.length - 1]?.rest + ? undefined + : declaration.parameters.length; + + if ( + node.arguments.length < minArgs || + (maxArgs !== undefined && node.arguments.length > maxArgs) + ) { + // In the case we have too little args then this decorator is not applicable. + // If there is too many args then we can still run the decorator as long as the args are valid. + if (node.arguments.length < minArgs) { + hasError = true; + } + + if (maxArgs === undefined) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument-count", + messageId: "atLeast", + format: { actual: node.arguments.length.toString(), expected: minArgs.toString() }, + target: node, + }) + ); + } else { + const expected = minArgs === maxArgs ? minArgs.toString() : `${minArgs}-${maxArgs}`; + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument-count", + format: { actual: node.arguments.length.toString(), expected }, + target: node, + }) + ); + } + } + + const resolvedArgs: Value[] = []; + + for (const [index, parameter] of declaration.parameters.entries()) { + if (parameter.rest) { + const restType = + parameter.type.kind === "ParamConstraintUnion" || parameter.type.kind === "Value" // TODO: change if we change this to not be a FunctionParameter + ? undefined + : getIndexType(parameter.type); + if (restType) { + for (let i = index; i < node.arguments.length; i++) { + const argNode = node.arguments[i]; + if (argNode) { + const arg = getValueForNode(argNode, mapper, restType); + if (arg === null) { + hasError = true; + continue; + } + if (checkValueOfType(arg, restType, argNode)) { + resolvedArgs.push(arg); + } else { + hasError = true; + } + } + } + } + break; + } + const argNode = node.arguments[index]; + if (argNode) { + const arg = getValueForNode(argNode, mapper, parameter.type as any); // TODO: change if we change this to not be a FunctionParameter + if (arg === null) { + hasError = true; + continue; + } + if (checkValueOfType(arg, parameter.type as any, argNode)) { + resolvedArgs.push(arg); + } else { + hasError = true; + } + } + } + if (hasError) { + return null; + } + return { + valueKind: "ScalarValue", + value: { + name: declaration.name, + args: resolvedArgs, + }, + scalar: declaration.scalar, + type: declaration.scalar, + }; + } + // TODO: should those be called eval? function checkCallExpression( node: CallExpressionNode, @@ -3437,19 +3536,7 @@ export function createChecker(program: Program): Checker { return null; } if (target.kind === "ScalarConstructor") { - const args = node.arguments.map((x) => getValueForNode(x, mapper)); - if (args.some((x) => x === null)) { - return null; - } - return { - valueKind: "ScalarValue", - value: { - name: target.name, - args: args as Value[], - }, - scalar: target.scalar, - type: target, - }; + return createScalarValue(node, mapper, target); } if (areScalarsRelated(target, getStdType("string"))) { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index d37a69c09d..e1103c801f 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -413,7 +413,7 @@ const diagnostics = { "non-callable": { severity: "error", messages: { - default: paramMessage`Type ${"typeKind"} is not is not callable.`, + default: paramMessage`Type ${"type"} is not is not callable.`, }, }, "named-init-required": { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index c8043b807a..2353b59bfd 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -797,7 +797,7 @@ export const enum SymbolFlags { * Symbols whose members will be late bound (and stored on the type) */ MemberContainer = Model | Enum | Union | Interface | Scalar, - Member = ModelProperty | EnumMember | UnionVariant | InterfaceMember, + Member = ModelProperty | EnumMember | UnionVariant | InterfaceMember | ScalarMember, } /** diff --git a/packages/compiler/test/checker/values/scalar-values.test.ts b/packages/compiler/test/checker/values/scalar-values.test.ts new file mode 100644 index 0000000000..459992278e --- /dev/null +++ b/packages/compiler/test/checker/values/scalar-values.test.ts @@ -0,0 +1,183 @@ +import { strictEqual } from "assert"; +import { describe, expect, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValueType, diagnoseValueType } from "./utils.js"; + +describe("instantiate with named constructor", () => { + const ipv4Code = ` + scalar ipv4 { + init fromString(value: string); + init fromBytes(a: uint8, b: uint8, c: uint8, d: uint8); + } + `; + + it("with single arg", async () => { + const value = await compileValueType(`ipv4.fromString("0.0.1.1")`, ipv4Code); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "ipv4"); + strictEqual(value.scalar?.name, "ipv4"); + strictEqual(value.value.name, "fromString"); + expect(value.value.args).toEqual([ + expect.objectContaining({ + value: "0.0.1.1", + valueKind: "StringValue", + }), + ]); + }); + + it("with multiple args", async () => { + const value = await compileValueType(`ipv4.fromBytes(0, 0, 1, 1)`, ipv4Code); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "ipv4"); + strictEqual(value.scalar?.name, "ipv4"); + strictEqual(value.value.name, "fromBytes"); + expect(value.value.args).toEqual([ + expect.objectContaining({ + valueKind: "NumericValue", + }), + expect.objectContaining({ + valueKind: "NumericValue", + }), + expect.objectContaining({ + valueKind: "NumericValue", + }), + expect.objectContaining({ + valueKind: "NumericValue", + }), + ]); + }); + + it("instantiate from another scalar", async () => { + const value = await compileValueType( + `b.fromA(a.fromString("a"))`, + ` + scalar a { init fromString(val: string);} + scalar b { init fromA(val: a);} + ` + ); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "b"); + strictEqual(value.scalar?.name, "b"); + strictEqual(value.value.name, "fromA"); + expect(value.value.args).toHaveLength(1); + const arg = value.value.args[0]; + strictEqual(arg.valueKind, "ScalarValue"); + strictEqual(arg.type.kind, "Scalar"); + strictEqual(arg.type.name, "a"); + }); + + it("emit warning if passing wrong type to constructor", async () => { + const diagnostics = await diagnoseValueType(`ipv4.fromString(123)`, ipv4Code); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '123' is not assignable to type 'string'", + }); + }); + + it("emit warning if passing too many args", async () => { + const diagnostics = await diagnoseValueType(`ipv4.fromString("abc", "def")`, ipv4Code); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected 1 arguments, but got 2.", + }); + }); + + it("emit warning if passing too few args", async () => { + const diagnostics = await diagnoseValueType(`ipv4.fromBytes(0, 0, 0)`, ipv4Code); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected 4 arguments, but got 3.", + }); + }); + + describe("with optional params", () => { + it("allow not providing it", async () => { + const value = await compileValueType( + `ipv4.fromItems("a")`, + ` + scalar ipv4 { + init fromItems(a: string, b?: string); + } + ` + ); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.value.name, "fromItems"); + expect(value.value.args).toHaveLength(1); + }); + it("allow providing it", async () => { + const value = await compileValueType( + `ipv4.fromItems("a", "b")`, + ` + scalar ipv4 { + init fromItems(a: string, b?: string); + } + ` + ); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.value.name, "fromItems"); + expect(value.value.args).toHaveLength(2); + }); + + it("emit warning if passing wrong type to constructor", async () => { + const diagnostics = await diagnoseValueType( + `ipv4.fromItems("a", 123)`, + ` + scalar ipv4 { + init fromItems(...value: string[]); + } + ` + ); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '123' is not assignable to type 'string'", + }); + }); + }); + describe("with rest params", () => { + it("support rest params", async () => { + const value = await compileValueType( + `ipv4.fromItems("a", "b", "c")`, + ` + scalar ipv4 { + init fromItems(...value: string[]); + } + ` + ); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.value.name, "fromItems"); + expect(value.value.args).toHaveLength(3); + }); + + it("support rest params with positional before", async () => { + const value = await compileValueType( + `ipv4.fromItems(1, "b", "c")`, + ` + scalar ipv4 { + init fromItems(value: int32, ...value: string[]); + } + ` + ); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.value.name, "fromItems"); + expect(value.value.args).toHaveLength(3); + }); + + it("emit warning if passing wrong type to constructor", async () => { + const diagnostics = await diagnoseValueType( + `ipv4.fromItems(123)`, + ` + scalar ipv4 { + init fromItems(...value: string[]); + } + ` + ); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '123' is not assignable to type 'string'", + }); + }); + }); +}); From bb357b7b4f3ce8ed8ef1363d1aa5a3befb583193 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 15:35:01 -0700 Subject: [PATCH 063/184] Better way to resolve values vs types --- packages/compiler/src/core/checker.ts | 243 ++++++++++-------- packages/compiler/src/core/type-utils.ts | 2 +- .../test/checker/values/array-values.test.ts | 83 ++++++ .../test/checker/values/object-values.test.ts | 148 +++++++++++ .../test/checker/values/values.test.ts | 220 ---------------- 5 files changed, 371 insertions(+), 325 deletions(-) create mode 100644 packages/compiler/test/checker/values/array-values.test.ts create mode 100644 packages/compiler/test/checker/values/object-values.test.ts delete mode 100644 packages/compiler/test/checker/values/values.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 470686e4e2..97d0e454b3 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,12 +1,5 @@ import { $docFromComment, getIndexer, isArrayModelType } from "../lib/decorators.js"; -import { - MultiKeyMap, - Mutable, - createRekeyableMap, - isArray, - isDefined, - mutate, -} from "../utils/misc.js"; +import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; import { createModelToLiteralCodeFix } from "./compiler-code-fixes/model-to-literal.codefix.js"; @@ -277,7 +270,7 @@ export interface Checker { resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]]; /** @internal */ - getTypeOrValueForNode(node: Node): Type | Value; + getTypeOrValueForNode(node: Node): Type | Value | null; errorType: ErrorType; voidType: VoidType; @@ -671,6 +664,9 @@ export function createChecker(program: Program): Checker { function getTypeForNode(node: Node, mapper?: TypeMapper): Type { const typeOrValue = getTypeOrValueForNode(node, mapper, undefined); + if (typeOrValue === null) { + return errorType; + } if (isValue(typeOrValue)) { reportCheckerDiagnostic( createDiagnostic({ @@ -684,41 +680,113 @@ export function createChecker(program: Program): Checker { } function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | null { - switch (node.kind) { - case SyntaxKind.ObjectLiteral: - return checkObjectLiteral(node, mapper); - case SyntaxKind.TupleLiteral: - return checkTupleLiteral(node, mapper); - case SyntaxKind.ConstStatement: - return checkConst(node); - case SyntaxKind.StringLiteral: - case SyntaxKind.StringTemplateExpression: - return checkStringValue(node, mapper, constraint); - case SyntaxKind.NumericLiteral: - return checkNumericValue(node, constraint); - case SyntaxKind.BooleanLiteral: - return checkBooleanValue(node, constraint); - case SyntaxKind.TypeReference: - return checkValueReference(node, mapper); - case SyntaxKind.CallExpression: - return checkCallExpression(node, mapper); + const entity = getTypeOrValueForNode(node, mapper, constraint); + if (entity === null || isValue(entity)) { + return entity; + } + // Some type can be converted to value in the right context + switch (entity.kind) { + case "String": + case "StringTemplate": + return checkStringValue(entity, mapper, constraint); + case "Number": + return checkNumericValue(entity, constraint); + case "Boolean": + return checkBooleanValue(entity, constraint); default: reportCheckerDiagnostic( createDiagnostic({ code: "expect-value", - format: { name: "?" }, // TODO: better message + format: { name: getTypeName(entity) }, target: node, }) ); return null; } + // switch (node.kind) { + // case SyntaxKind.ObjectLiteral: + // return checkObjectValue(node, mapper); + // case SyntaxKind.TupleLiteral: + // return checkTupleValue(node, mapper); + // case SyntaxKind.ConstStatement: + // return checkConst(node); + // case SyntaxKind.StringLiteral: + // case SyntaxKind.StringTemplateExpression: + // return checkStringValue(node, mapper, constraint); + // case SyntaxKind.NumericLiteral: + // return checkNumericValue(node, constraint); + // case SyntaxKind.BooleanLiteral: + // return checkBooleanValue(node, constraint); + // case SyntaxKind.TypeReference: + // return checkValueReference(node, mapper); + // case SyntaxKind.CallExpression: + // return checkCallExpression(node, mapper); + // default: + // reportCheckerDiagnostic( + // createDiagnostic({ + // code: "expect-value", + // format: { name: "?" }, // TODO: better message + // target: node, + // }) + // ); + // return null; + // } } + /** + * Gets a type or value depending on the node and current constraint. + * For nodes that can be both type or values(e.g. string), the value will be returned if the constraint expect a value of that type even if the constrain also allows the type. + * This means that if the constraint is `string | valueof string` passing `"abc"` will send the value `"abc"` and not the type `"abc"`. + */ function getTypeOrValueForNode( node: Node, mapper?: TypeMapper, constraint?: Type | ValueType | ParamConstraintUnion | undefined - ): Type | Value { + ): Type | Value | null { + const entity = getTypeOrValueForNodeInternal(node, mapper, constraint); + if (entity === null || isValue(entity)) { + return entity; + } + + const valueConstraint = extractValueOfConstraints(constraint); + if (valueConstraint) { + switch (entity.kind) { + case "String": + case "StringTemplate": + return checkStringValue(entity, mapper, valueConstraint); + case "Number": + return checkNumericValue(entity, valueConstraint); + case "Boolean": + return checkBooleanValue(entity, valueConstraint); + } + } + + return entity; + } + + /** Extact the type constraint a value should match. */ + function extractValueOfConstraints( + constraint: Type | ValueType | ParamConstraintUnion | undefined + ): Type | undefined { + if (constraint === undefined || isType(constraint)) { + return undefined; + } + if (constraint.kind === "Value") { + return constraint.target; + } else { + const valueOfOptions = constraint.options + .filter((x): x is ValueType => x.kind === "Value") + .map((x) => x.target); + return createUnion(valueOfOptions); + } + } + + /** Do not call to be used inside getTypeOrValueForNode */ + function getTypeOrValueForNodeInternal( + node: Node, + mapper?: TypeMapper, + constraint?: Type | ValueType | ParamConstraintUnion | undefined + ): Type | Value | null { switch (node.kind) { case SyntaxKind.ModelExpression: return checkModel(node, mapper); @@ -746,17 +814,11 @@ export function createChecker(program: Program): Checker { case SyntaxKind.OperationStatement: return checkOperation(node, mapper); case SyntaxKind.NumericLiteral: - return constraint?.kind === "Value" - ? checkNumericValue(node, constraint.target) - : checkNumericLiteral(node); + return checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: - return constraint?.kind === "Value" - ? checkBooleanValue(node, constraint.target) - : checkBooleanLiteral(node); + return checkBooleanLiteral(node); case SyntaxKind.StringLiteral: - return constraint?.kind === "Value" - ? checkStringValue(node, mapper, constraint.target) - : checkStringLiteral(node); + return checkStringLiteral(node); case SyntaxKind.TupleExpression: return checkTupleExpression(node, mapper); case SyntaxKind.StringTemplateExpression: @@ -786,13 +848,13 @@ export function createChecker(program: Program): Checker { case SyntaxKind.UnknownKeyword: return unknownType; case SyntaxKind.ObjectLiteral: - return checkObjectLiteral(node, mapper); + return checkObjectValue(node, mapper); case SyntaxKind.TupleLiteral: - return checkTupleLiteral(node, mapper); + return checkTupleValue(node, mapper); case SyntaxKind.ConstStatement: - return checkConst(node) ?? errorType; // TODO: do we want that? + return checkConst(node); case SyntaxKind.CallExpression: - return checkCallExpression(node, mapper) ?? errorType; // TODO: do we want that? + return checkCallExpression(node, mapper); default: return errorType; } @@ -935,7 +997,7 @@ export function createChecker(program: Program): Checker { function visit(node: Node) { const type = getTypeOrValueForNode(node); let hasError = false; - if ("kind" in type && type.kind === "TemplateParameter") { + if (type !== null && "kind" in type && type.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { if (type.node.symbol === templateParameters[i].symbol) { reportCheckerDiagnostic( @@ -1008,7 +1070,7 @@ export function createChecker(program: Program): Checker { function checkTemplateArgument( node: TemplateArgumentNode, mapper: TypeMapper | undefined - ): Type | Value { + ): Type | Value | null { return getTypeOrValueForNode(node.argument, mapper); } @@ -1121,7 +1183,7 @@ export function createChecker(program: Program): Checker { for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { function deferredCheck(): [Node, Type | Value] { - return [arg, getTypeOrValueForNode(arg.argument, mapper)]; + return [arg, getTypeOrValueForNode(arg.argument, mapper) ?? errorType]; } if (arg.name) { @@ -3163,10 +3225,7 @@ export function createChecker(program: Program): Checker { } } - function checkObjectLiteral( - node: ObjectLiteralNode, - mapper: TypeMapper | undefined - ): ObjectValue { + function checkObjectValue(node: ObjectLiteralNode, mapper: TypeMapper | undefined): ObjectValue { return { valueKind: "ObjectValue", node: node, @@ -3199,33 +3258,6 @@ export function createChecker(program: Program): Checker { return properties; } - function checkIsValue(type: Type | Value, diagnosticTarget: DiagnosticTarget): type is Value { - if (!isValue(type)) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "expect-value", - format: { name: getTypeName(type) }, - target: diagnosticTarget, - }) - ); - return false; - } - return true; - } - - function checkIsType(type: Type | Value, diagnosticTarget: DiagnosticTarget): type is Type { - if (!isType(type)) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "value-in-type", - target: diagnosticTarget, - }) - ); - return false; - } - return true; - } - function checkObjectSpreadProperty( targetNode: TypeReferenceNode, mapper: TypeMapper | undefined @@ -3242,21 +3274,25 @@ export function createChecker(program: Program): Checker { return value; } - function checkTupleLiteral(node: TupleLiteralNode, mapper: TypeMapper | undefined): ArrayValue { - const values = node.values - .map((itemNode) => { - const type = getTypeOrValueForNode(itemNode, mapper); - if (checkIsValue(type, itemNode)) { - return type; - } else { - return undefined; // TODO: do we want to omit this or include an error type? - } - }) - .filter(isDefined); + function checkTupleValue( + node: TupleLiteralNode, + mapper: TypeMapper | undefined + ): ArrayValue | null { + let hasError = false; + const values = node.values.map((itemNode) => { + const value = getValueForNode(itemNode, mapper); + if (value === null) { + hasError = true; + } + return value; + }); + if (hasError) { + return null; + } return { valueKind: "ArrayValue", node: node, - values, + values: values as any, type: null as any, // TODO: fix }; } @@ -3305,19 +3341,16 @@ export function createChecker(program: Program): Checker { } function checkStringValue( - node: StringLiteralNode | StringTemplateExpressionNode, + literalType: StringLiteral | StringTemplate, mapper: TypeMapper | undefined, type: Type | undefined ): StringValue { - let literalType: StringLiteral | StringTemplate; let value: string; - if (node.kind === SyntaxKind.StringTemplateExpression) { - literalType = checkStringTemplateExpresion(node, mapper); + if (literalType.kind === "StringTemplate") { const [result, diagnostics] = stringTemplateToString(literalType); value = result; reportCheckerDiagnostics(diagnostics); } else { - literalType = getLiteralType(node); value = literalType.value; } const scalar = inferScalarForPrimitiveValue(getStdType("string"), type, literalType); @@ -3329,24 +3362,21 @@ export function createChecker(program: Program): Checker { }; } - function checkNumericValue(node: NumericLiteralNode, type: Type | undefined): NumericValue { - const literalType = getLiteralType(node); - + function checkNumericValue(literalType: NumericLiteral, type: Type | undefined): NumericValue { const scalar = inferScalarForPrimitiveValue(getStdType("numeric"), type, literalType); return { valueKind: "NumericValue", - value: Numeric(node.valueAsString), + value: Numeric(literalType.valueAsString), type: literalType, scalar, }; } - function checkBooleanValue(node: BooleanLiteralNode, type: Type | undefined): BooleanValue { - const literalType = getLiteralType(node); + function checkBooleanValue(literalType: BooleanLiteral, type: Type | undefined): BooleanValue { const scalar = inferScalarForPrimitiveValue(getStdType("boolean"), type, literalType); return { valueKind: "BooleanValue", - value: node.value, + value: literalType.value, type: type ?? literalType, scalar, }; @@ -4219,7 +4249,7 @@ export function createChecker(program: Program): Checker { function checkDefault(defaultNode: Node, type: Type): Type | Value { const defaultType = getTypeOrValueForNode(defaultNode, undefined); - if (isErrorType(type)) { + if (defaultType === null || isErrorType(type)) { return errorType; } if (!isDefaultValue(defaultType)) { @@ -4340,7 +4370,7 @@ export function createChecker(program: Program): Checker { return [ false, node.arguments.map((argNode): DecoratorArgument => { - const type = getTypeOrValueForNode(argNode, mapper); + const type = getTypeOrValueForNode(argNode, mapper) ?? errorType; return { value: type, jsValue: type, @@ -4403,8 +4433,9 @@ export function createChecker(program: Program): Checker { const argNode = node.arguments[i]; if (argNode) { const arg = getTypeOrValueForNode(argNode, mapper, parameter.type); - if ( + arg !== null && + !(isType(arg) && isErrorType(arg)) && checkArgumentAssignable( arg, parameter.type.kind === "Value" ? { kind: "Value", target: restType } : restType, @@ -4427,7 +4458,11 @@ export function createChecker(program: Program): Checker { const argNode = node.arguments[index]; if (argNode) { const arg = getTypeOrValueForNode(argNode, mapper, parameter.type); - if (checkArgumentAssignable(arg, parameter.type, argNode)) { + if ( + arg !== null && + !(isType(arg) && isErrorType(arg)) && + checkArgumentAssignable(arg, parameter.type, argNode) + ) { resolvedArgs.push({ value: arg, node: argNode, diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index c71344ed0a..76b263b8b3 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -43,7 +43,7 @@ export function isNullType(type: Type): type is NullType { } export function isType(entity: Entity): entity is Type { - return "kind" in entity; + return "kind" in entity && entity.kind !== "Value" && entity.kind !== "ParamConstraintUnion"; } export function isValue(entity: Entity): entity is Value { return "valueKind" in entity; diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts new file mode 100644 index 0000000000..53b553b7d9 --- /dev/null +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -0,0 +1,83 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/index.js"; +import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; + +it("no values", async () => { + const object = await compileValueType(`#[]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 0); +}); + +it("single value", async () => { + const object = await compileValueType(`#["John"]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 1); + const first = object.values[0]; + strictEqual(first.valueKind, "StringValue"); + strictEqual(first.value, "John"); +}); + +it("multiple property", async () => { + const object = await compileValueType(`#["John", 21]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 2); + + const nameProp = object.values[0]; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.values[1]; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value, 21); +}); + +describe("valid property types", () => { + it.each([ + ["StringValue", `"John"`], + ["NumericValue", "21"], + ["Boolean", "true"], + ["EnumMember", "Direction.up", "enum Direction { up, down }"], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], + ])("%s", async (kind, type, other?) => { + const object = await compileValueType(`#[${type}]`, other); + strictEqual(object.valueKind, "ArrayValue"); + const nameProp = object.values[0]; + strictEqual(nameProp?.valueKind, kind); + }); +}); + +it("emit diagnostic if referencing a non literal type", async () => { + const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "(anonymous model) refers to a type, but is being used as a value here.", + }); +}); + +describe("emit diagnostic when used in", () => { + it("emit diagnostic when used in a model", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test { + prop: ┆#["John"]; + } + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + + it("emit diagnostic when used in template constraint", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); +}); diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts new file mode 100644 index 0000000000..c18cd6bffc --- /dev/null +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -0,0 +1,148 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/index.js"; +import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; + +it("no properties", async () => { + const object = await compileValueType(`#{}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 0); +}); + +it("single property", async () => { + const object = await compileValueType(`#{name: "John"}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 1); + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); +}); + +it("multiple property", async () => { + const object = await compileValueType(`#{name: "John", age: 21}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 2); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.properties.get("age")?.value; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value.asNumber(), 21); +}); + +describe("spreading", () => { + it("add the properties", async () => { + const object = await compileValueType( + `#{...Common, age: 21}`, + `const Common = #{ name: "John" };` + ); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 2); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + + const ageProp = object.properties.get("age")?.value; + strictEqual(ageProp?.valueKind, "NumericValue"); + strictEqual(ageProp.value.asNumber(), 21); + }); + + it("override properties defined before if there is a name conflict", async () => { + const object = await compileValueType( + `#{name: "John", age: 21, ...Common, }`, + `const Common = #{ name: "Common" };` + ); + strictEqual(object.valueKind, "ObjectValue"); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "Common"); + }); + + it("override properties spread before", async () => { + const object = await compileValueType( + `#{...Common, name: "John", age: 21 }`, + `const Common = #{ name: "John" };` + ); + strictEqual(object.valueKind, "ObjectValue"); + + const nameProp = object.properties.get("name")?.value; + strictEqual(nameProp?.valueKind, "StringValue"); + strictEqual(nameProp.value, "John"); + }); + + it("emit diagnostic is spreading a model", async () => { + const diagnostics = await diagnoseValueType( + `#{...Common, age: 21}`, + `const Common = { name: "John" };` + ); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "? refers to a type, but is being used as a value here.", + }); + }); + + it("emit diagnostic is spreading a non-object values", async () => { + const diagnostics = await diagnoseValueType( + `#{...Common, age: 21}`, + `const Common = #["abc"];` + ); + expectDiagnostics(diagnostics, { + code: "spread-object", + message: "Cannot spread properties of non-object type.", + }); + }); +}); + +describe("valid property types", () => { + it.each([ + ["StringValue", `"John"`], + ["NumericValue", "21"], + ["BooleanValue", "true"], + ["EnumMember", "Direction.up", "enum Direction { up, down }"], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], + ])("%s", async (kind, type, other?) => { + const object = await compileValueType(`#{prop: ${type}}`, other); + strictEqual(object.valueKind, "ObjectValue"); + const nameProp = object.properties.get("prop")?.value; + strictEqual(nameProp?.valueKind, kind); + }); +}); + +it("emit diagnostic if referencing a non literal type", async () => { + const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "(anonymous model) refers to a type, but is being used as a value here.", + }); +}); + +describe("emit diagnostic when used in", () => { + it("emit diagnostic when used in a model", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test { + prop: ┆#{ name: "John" }; + } + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); + + it("emit diagnostic when used in template constraint", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + `); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + }); + }); +}); diff --git a/packages/compiler/test/checker/values/values.test.ts b/packages/compiler/test/checker/values/values.test.ts deleted file mode 100644 index 59872ab43c..0000000000 --- a/packages/compiler/test/checker/values/values.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { strictEqual } from "assert"; -import { describe, it } from "vitest"; -import { expectDiagnostics } from "../../../src/testing/index.js"; -import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; - -describe("object literals", () => { - it("no properties", async () => { - const object = await compileValueType(`#{}`); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 0); - }); - - it("single property", async () => { - const object = await compileValueType(`#{name: "John"}`); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 1); - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - }); - - it("multiple property", async () => { - const object = await compileValueType(`#{name: "John", age: 21}`); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 2); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - - const ageProp = object.properties.get("age")?.value; - strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value.asNumber(), 21); - }); - - describe("spreading", () => { - it("add the properties", async () => { - const object = await compileValueType( - `#{...Common, age: 21}`, - `alias Common = #{ name: "John" };` - ); - strictEqual(object.valueKind, "ObjectValue"); - strictEqual(object.properties.size, 2); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - - const ageProp = object.properties.get("age")?.value; - strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value.asNumber(), 21); - }); - - it("override properties defined before if there is a name conflict", async () => { - const object = await compileValueType( - `#{name: "John", age: 21, ...Common, }`, - `alias Common = #{ name: "Common" };` - ); - strictEqual(object.valueKind, "ObjectValue"); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "Common"); - }); - - it("override properties spread before", async () => { - const object = await compileValueType( - `#{...Common, name: "John", age: 21 }`, - `alias Common = #{ name: "John" };` - ); - strictEqual(object.valueKind, "ObjectValue"); - - const nameProp = object.properties.get("name")?.value; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - }); - - it("emit diagnostic is spreading something else than an object literal", async () => { - const diagnostics = await diagnoseValueType( - `#{...Common, age: 21}`, - `alias Common = { name: "John" };` - ); - expectDiagnostics(diagnostics, { - code: "spread-object", - message: "Cannot spread properties of non-object type.", - }); - }); - }); - - describe("valid property types", () => { - it.each([ - ["StringValue", `"John"`], - ["NumericValue", "21"], - ["Boolean", "true"], - ["EnumMember", "Direction.up", "enum Direction { up, down }"], - ["ObjectValue", `#{nested: "foo"}`], - ["ArrayValue", `#["foo"]`], - ])("%s", async (kind, type, other?) => { - const object = await compileValueType(`#{prop: ${type}}`, other); - strictEqual(object.valueKind, "ObjectValue"); - const nameProp = object.properties.get("prop")?.value; - strictEqual(nameProp?.valueKind, kind); - }); - }); - - it("emit diagnostic if referencing a non literal type", async () => { - const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); - expectDiagnostics(diagnostics, { - code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", - }); - }); - - describe("emit diagnostic when used in", () => { - it("emit diagnostic when used in a model", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test { - prop: ┆#{ name: "John" }; - } - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - - it("emit diagnostic when used in template constraint", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test {} - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - }); -}); - -describe("tuple literals", () => { - it("no values", async () => { - const object = await compileValueType(`#[]`); - strictEqual(object.valueKind, "ArrayValue"); - strictEqual(object.values.length, 0); - }); - - it("single value", async () => { - const object = await compileValueType(`#["John"]`); - strictEqual(object.valueKind, "ArrayValue"); - strictEqual(object.values.length, 1); - const first = object.values[0]; - strictEqual(first.valueKind, "StringValue"); - strictEqual(first.value, "John"); - }); - - it("multiple property", async () => { - const object = await compileValueType(`#["John", 21]`); - strictEqual(object.valueKind, "ArrayValue"); - strictEqual(object.values.length, 2); - - const nameProp = object.values[0]; - strictEqual(nameProp?.valueKind, "StringValue"); - strictEqual(nameProp.value, "John"); - - const ageProp = object.values[1]; - strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value, 21); - }); - - describe("valid property types", () => { - it.each([ - ["StringValue", `"John"`], - ["NumericValue", "21"], - ["Boolean", "true"], - ["EnumMember", "Direction.up", "enum Direction { up, down }"], - ["ObjectValue", `#{nested: "foo"}`], - ["ArrayValue", `#["foo"]`], - ])("%s", async (kind, type, other?) => { - const object = await compileValueType(`#[${type}]`, other); - strictEqual(object.valueKind, "ArrayValue"); - const nameProp = object.values[0]; - strictEqual(nameProp?.valueKind, kind); - }); - }); - - it("emit diagnostic if referencing a non literal type", async () => { - const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); - expectDiagnostics(diagnostics, { - code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", - }); - }); - - describe("emit diagnostic when used in", () => { - it("emit diagnostic when used in a model", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test { - prop: ┆#["John"]; - } - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - - it("emit diagnostic when used in template constraint", async () => { - const { diagnostics, pos } = await diagnoseUsage(` - model Test {} - `); - expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", - pos, - }); - }); - }); -}); From de6f1390c8a24bbb2bfd5c6c67ff3e610ec42f66 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 16:06:45 -0700 Subject: [PATCH 064/184] Progress --- packages/compiler/src/core/checker.ts | 145 +++++++++++------- packages/compiler/src/core/types.ts | 8 +- .../test/checker/values/array-values.test.ts | 7 +- .../test/checker/values/object-values.test.ts | 7 +- 4 files changed, 102 insertions(+), 65 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 97d0e454b3..156e00ab78 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5,7 +5,12 @@ import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-iden import { createModelToLiteralCodeFix } from "./compiler-code-fixes/model-to-literal.codefix.js"; import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; -import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; +import { + ProjectionError, + compilerAssert, + ignoreDiagnostics, + reportDeprecated, +} from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { TypeNameOptions, @@ -63,6 +68,7 @@ import { EnumMember, EnumMemberNode, EnumStatementNode, + EnumValue, ErrorType, Expression, FunctionDeclarationStatementNode, @@ -96,6 +102,8 @@ import { NeverType, Node, NodeFlags, + NullType, + NullValue, NumericLiteral, NumericLiteralNode, NumericValue, @@ -680,59 +688,49 @@ export function createChecker(program: Program): Checker { } function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | null { - const entity = getTypeOrValueForNode(node, mapper, constraint); + let entity = getTypeOrValueForNodeInternal(node, mapper); + if (entity === null || isValue(entity)) { + return entity; + } + entity = tryUsingValueOfType(entity, mapper, constraint); if (entity === null || isValue(entity)) { return entity; } - // Some type can be converted to value in the right context - switch (entity.kind) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity) }, + target: node, + }) + ); + return null; + } + + function tryUsingValueOfType( + type: Type, + mapper: TypeMapper | undefined, + constraint: Type | undefined + ): Type | Value | null { + switch (type.kind) { case "String": case "StringTemplate": - return checkStringValue(entity, mapper, constraint); + return checkStringValue(type, mapper, constraint); case "Number": - return checkNumericValue(entity, constraint); + return checkNumericValue(type, constraint); case "Boolean": - return checkBooleanValue(entity, constraint); + return checkBooleanValue(type, constraint); + case "EnumMember": + return checkEnumValue(type, constraint); + case "Intrinsic": + switch (type.name) { + case "null": + return checkNullValue(type as any, constraint); + } + return type; default: - reportCheckerDiagnostic( - createDiagnostic({ - code: "expect-value", - format: { name: getTypeName(entity) }, - target: node, - }) - ); - return null; - } - // switch (node.kind) { - // case SyntaxKind.ObjectLiteral: - // return checkObjectValue(node, mapper); - // case SyntaxKind.TupleLiteral: - // return checkTupleValue(node, mapper); - // case SyntaxKind.ConstStatement: - // return checkConst(node); - // case SyntaxKind.StringLiteral: - // case SyntaxKind.StringTemplateExpression: - // return checkStringValue(node, mapper, constraint); - // case SyntaxKind.NumericLiteral: - // return checkNumericValue(node, constraint); - // case SyntaxKind.BooleanLiteral: - // return checkBooleanValue(node, constraint); - // case SyntaxKind.TypeReference: - // return checkValueReference(node, mapper); - // case SyntaxKind.CallExpression: - // return checkCallExpression(node, mapper); - // default: - // reportCheckerDiagnostic( - // createDiagnostic({ - // code: "expect-value", - // format: { name: "?" }, // TODO: better message - // target: node, - // }) - // ); - // return null; - // } + return type; + } } - /** * Gets a type or value depending on the node and current constraint. * For nodes that can be both type or values(e.g. string), the value will be returned if the constraint expect a value of that type even if the constrain also allows the type. @@ -750,15 +748,7 @@ export function createChecker(program: Program): Checker { const valueConstraint = extractValueOfConstraints(constraint); if (valueConstraint) { - switch (entity.kind) { - case "String": - case "StringTemplate": - return checkStringValue(entity, mapper, valueConstraint); - case "Number": - return checkNumericValue(entity, valueConstraint); - case "Boolean": - return checkBooleanValue(entity, valueConstraint); - } + return tryUsingValueOfType(entity, mapper, valueConstraint); } return entity; @@ -3297,6 +3287,26 @@ export function createChecker(program: Program): Checker { }; } + function findTypesMatching(base: Type, constraint: Type): Type[] { + if (constraint.kind === base.kind) { + if (ignoreDiagnostics(isTypeAssignableTo(base, constraint, base))) { + return [constraint]; + } + return []; + } else if (constraint.kind === "Union") { + const matches: Type[] = []; + for (const variant of constraint.variants.values()) { + const subMatches = findTypesMatching(base, variant.type); + for (const match of subMatches) { + matches.push(match); + } + } + return matches; + } else { + return []; + } + } + function inferScalarForPrimitiveValue( base: Scalar, type: Type | undefined, @@ -3310,7 +3320,7 @@ export function createChecker(program: Program): Checker { if (areScalarsRelated(type, base)) { return type; } - return undefined; // TODO: do i need to report an error here + return undefined; case "Union": let found = undefined; for (const variant of type.variants.values()) { @@ -3382,6 +3392,31 @@ export function createChecker(program: Program): Checker { }; } + function checkNullValue(literalType: NullType, type: Type | undefined): NullValue | null { + if ( + type !== undefined && + !ignoreDiagnostics(isTypeAssignableTo(literalType, type, literalType)) + ) { + return null; + } + return { + valueKind: "NullValue", + type: type ?? literalType, + value: null, + }; + } + + function checkEnumValue(literalType: EnumMember, type: Type | undefined): EnumValue | null { + if (type !== undefined && !findTypesMatching(literalType, type)) { + return null; + } + return { + valueKind: "EnumValue", + type: type ?? literalType, + value: literalType, + }; + } + /** * Check and resolve a type for the given type reference node. * @param node Node. diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 2353b59bfd..bad770be88 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -14,7 +14,7 @@ Value extends StringValue ? string : Value extends BooleanValue ? boolean : Value extends ObjectValue ? Record : Value extends ArrayValue ? unknown[] - : Value extends EnumMemberValue ? EnumMember + : Value extends EnumValue ? EnumMember : Value extends NullValue ? null : Value extends ScalarValue ? Value : Value @@ -313,7 +313,7 @@ export type Value = | BooleanValue | ObjectValue | ArrayValue - | EnumMemberValue + | EnumValue | NullValue; interface BaseValue { @@ -358,8 +358,8 @@ export interface BooleanValue extends BaseValue { scalar: Scalar | undefined; value: boolean; } -export interface EnumMemberValue extends BaseValue { - valueKind: "EnumMemberValue"; +export interface EnumValue extends BaseValue { + valueKind: "EnumValue"; value: EnumMember; } export interface NullValue extends BaseValue { diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index 53b553b7d9..8d7507a040 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -29,15 +29,16 @@ it("multiple property", async () => { const ageProp = object.values[1]; strictEqual(ageProp?.valueKind, "NumericValue"); - strictEqual(ageProp.value, 21); + strictEqual(ageProp.value.asNumber(), 21); }); describe("valid property types", () => { it.each([ ["StringValue", `"John"`], ["NumericValue", "21"], - ["Boolean", "true"], - ["EnumMember", "Direction.up", "enum Direction { up, down }"], + ["BooleanValue", "true"], + ["NullValue", "null"], + ["EnumValue", "Direction.up", "enum Direction { up, down }"], ["ObjectValue", `#{nested: "foo"}`], ["ArrayValue", `#["foo"]`], ])("%s", async (kind, type, other?) => { diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index c18cd6bffc..463423b4ca 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -77,11 +77,11 @@ describe("spreading", () => { it("emit diagnostic is spreading a model", async () => { const diagnostics = await diagnoseValueType( `#{...Common, age: 21}`, - `const Common = { name: "John" };` + `alias Common = { name: "John" };` ); expectDiagnostics(diagnostics, { code: "expect-value", - message: "? refers to a type, but is being used as a value here.", + message: "(anonymous model) refers to a type, but is being used as a value here.", }); }); @@ -102,7 +102,8 @@ describe("valid property types", () => { ["StringValue", `"John"`], ["NumericValue", "21"], ["BooleanValue", "true"], - ["EnumMember", "Direction.up", "enum Direction { up, down }"], + ["NullValue", "null"], + ["EnumValue", "Direction.up", "enum Direction { up, down }"], ["ObjectValue", `#{nested: "foo"}`], ["ArrayValue", `#["foo"]`], ])("%s", async (kind, type, other?) => { From 04553168a019e747b30afc2bb4cffb1e1ddace04 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 16:26:17 -0700 Subject: [PATCH 065/184] Type --- packages/compiler/src/core/checker.ts | 47 +++++++++++++++++-- .../src/core/helpers/type-name-utils.ts | 2 +- packages/compiler/src/core/js-marshaller.ts | 2 +- .../test/checker/values/const.test.ts | 15 ++++++ 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/checker/values/const.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 156e00ab78..5635a014f8 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -840,7 +840,7 @@ export function createChecker(program: Program): Checker { case SyntaxKind.ObjectLiteral: return checkObjectValue(node, mapper); case SyntaxKind.TupleLiteral: - return checkTupleValue(node, mapper); + return checkArrayValue(node, mapper); case SyntaxKind.ConstStatement: return checkConst(node); case SyntaxKind.CallExpression: @@ -3216,14 +3216,43 @@ export function createChecker(program: Program): Checker { } function checkObjectValue(node: ObjectLiteralNode, mapper: TypeMapper | undefined): ObjectValue { + const properties = checkObjectLiteralProperties(node, mapper); return { valueKind: "ObjectValue", node: node, - properties: checkObjectLiteralProperties(node, mapper), - type: null as any, // TODO: fix + properties, + type: createTypeForObjectValue(properties), }; } + function createTypeForObjectValue(properties: Map): Model { + return createAndFinishType({ + kind: "Model", + name: "", + properties: createRekeyableMap( + [...properties.entries()].map(([name, prop]) => [ + name, + createModelPropertyForObjectPropertyDescriptor(prop), + ]) + ), + decorators: [], + derivedModels: [], + }); + } + + function createModelPropertyForObjectPropertyDescriptor( + prop: ObjectValuePropertyDescriptor + ): ModelProperty { + return createAndFinishType({ + kind: "ModelProperty", + node: undefined!, + optional: false, + name: prop.name, + type: prop.value.type, + decorators: [], + }); + } + function checkObjectLiteralProperties( node: ObjectLiteralNode, mapper: TypeMapper | undefined @@ -3264,7 +3293,7 @@ export function createChecker(program: Program): Checker { return value; } - function checkTupleValue( + function checkArrayValue( node: TupleLiteralNode, mapper: TypeMapper | undefined ): ArrayValue | null { @@ -3283,10 +3312,18 @@ export function createChecker(program: Program): Checker { valueKind: "ArrayValue", node: node, values: values as any, - type: null as any, // TODO: fix + type: createTypeForArrayValue(values as any), }; } + function createTypeForArrayValue(values: Value[]): Tuple { + return createAndFinishType({ + kind: "Tuple", + node: undefined!, + values: values.map((x) => x.type), + }); + } + function findTypesMatching(base: Type, constraint: Type): Type[] { if (constraint.kind === base.kind) { if (ignoreDiagnostics(isTypeAssignableTo(base, constraint, base))) { diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index ad626aecc6..5dfefda137 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -73,7 +73,7 @@ function getValuePreview(value: Value, options?: TypeNameOptions): string { return `${value.value}`; case "NumericValue": return `${value.value.toString()}`; - case "EnumMemberValue": + case "EnumValue": return getTypeName(value.value); case "NullValue": return "null"; diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 4f5800cef7..346e6c0377 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -40,7 +40,7 @@ export function marshallTypeForJS(type: T): MarshalledValue return objectValueToJs(type) as any; case "ArrayValue": return arrayValueToJs(type) as any; - case "EnumMemberValue": + case "EnumValue": return type.value as any; case "NullValue": return null as any; diff --git a/packages/compiler/test/checker/values/const.test.ts b/packages/compiler/test/checker/values/const.test.ts new file mode 100644 index 0000000000..11bf58c8b3 --- /dev/null +++ b/packages/compiler/test/checker/values/const.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { compileValueType } from "./utils.js"; + +describe("without type it use the most precise type", () => { + it.each([ + ["1", "Number"], + [`"abc"`, "String"], + [`true`, "Boolean"], + [`#{foo: "abc"}`, "Model"], + [`#["abc"]`, "Tuple"], + ])("%s => %s", async (input, kind) => { + const value = await compileValueType("a", `const a = ${input};`); + expect(value.type.kind).toBe(kind); + }); +}); From 795358029c9f29203050565adbfa6b18a7e8959a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 18:18:17 -0700 Subject: [PATCH 066/184] more tests --- packages/compiler/src/core/checker.ts | 126 +++++++++++------- .../test/checker/values/const.test.ts | 52 +++++++- .../test/checker/values/string-values.test.ts | 2 +- .../compiler/test/checker/values/utils.ts | 13 +- 4 files changed, 136 insertions(+), 57 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5635a014f8..e777449233 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -688,11 +688,11 @@ export function createChecker(program: Program): Checker { } function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | null { - let entity = getTypeOrValueForNodeInternal(node, mapper); + let entity = getTypeOrValueForNodeInternal(node, mapper, constraint); if (entity === null || isValue(entity)) { return entity; } - entity = tryUsingValueOfType(entity, mapper, constraint); + entity = tryUsingValueOfType(entity, mapper, constraint, node); if (entity === null || isValue(entity)) { return entity; } @@ -709,22 +709,29 @@ export function createChecker(program: Program): Checker { function tryUsingValueOfType( type: Type, mapper: TypeMapper | undefined, - constraint: Type | undefined + constraint: Type | undefined, + node: Node ): Type | Value | null { + if ( + constraint !== undefined && + !ignoreDiagnostics(isTypeAssignableTo(type, type, constraint)) + ) { + return null; + } switch (type.kind) { case "String": case "StringTemplate": - return checkStringValue(type, mapper, constraint); + return checkStringValue(type, constraint, node); case "Number": - return checkNumericValue(type, constraint); + return checkNumericValue(type, constraint, node); case "Boolean": - return checkBooleanValue(type, constraint); + return checkBooleanValue(type, constraint, node); case "EnumMember": - return checkEnumValue(type, constraint); + return checkEnumValue(type, constraint, node); case "Intrinsic": switch (type.name) { case "null": - return checkNullValue(type as any, constraint); + return checkNullValue(type as any, constraint, node); } return type; default: @@ -741,14 +748,14 @@ export function createChecker(program: Program): Checker { mapper?: TypeMapper, constraint?: Type | ValueType | ParamConstraintUnion | undefined ): Type | Value | null { - const entity = getTypeOrValueForNodeInternal(node, mapper, constraint); + const valueConstraint = extractValueOfConstraints(constraint); + const entity = getTypeOrValueForNodeInternal(node, mapper, valueConstraint); if (entity === null || isValue(entity)) { return entity; } - const valueConstraint = extractValueOfConstraints(constraint); if (valueConstraint) { - return tryUsingValueOfType(entity, mapper, valueConstraint); + return tryUsingValueOfType(entity, mapper, valueConstraint, node); } return entity; @@ -775,7 +782,7 @@ export function createChecker(program: Program): Checker { function getTypeOrValueForNodeInternal( node: Node, mapper?: TypeMapper, - constraint?: Type | ValueType | ParamConstraintUnion | undefined + valueConstraint?: Type | undefined ): Type | Value | null { switch (node.kind) { case SyntaxKind.ModelExpression: @@ -838,9 +845,10 @@ export function createChecker(program: Program): Checker { case SyntaxKind.UnknownKeyword: return unknownType; case SyntaxKind.ObjectLiteral: - return checkObjectValue(node, mapper); + console.trace("Checking", valueConstraint); + return checkObjectValue(node, mapper, valueConstraint); case SyntaxKind.TupleLiteral: - return checkArrayValue(node, mapper); + return checkArrayValue(node, mapper, valueConstraint); case SyntaxKind.ConstStatement: return checkConst(node); case SyntaxKind.CallExpression: @@ -3215,13 +3223,21 @@ export function createChecker(program: Program): Checker { } } - function checkObjectValue(node: ObjectLiteralNode, mapper: TypeMapper | undefined): ObjectValue { + function checkObjectValue( + node: ObjectLiteralNode, + mapper: TypeMapper | undefined, + type: Type | undefined + ): ObjectValue | null { const properties = checkObjectLiteralProperties(node, mapper); + const preciseType = createTypeForObjectValue(properties); + if (type && !checkTypeAssignable(preciseType, type, node)) { + return null; + } return { valueKind: "ObjectValue", node: node, properties, - type: createTypeForObjectValue(properties), + type: type ?? preciseType, }; } @@ -3295,7 +3311,8 @@ export function createChecker(program: Program): Checker { function checkArrayValue( node: TupleLiteralNode, - mapper: TypeMapper | undefined + mapper: TypeMapper | undefined, + type: Type | undefined ): ArrayValue | null { let hasError = false; const values = node.values.map((itemNode) => { @@ -3308,11 +3325,17 @@ export function createChecker(program: Program): Checker { if (hasError) { return null; } + + const preciseType = createTypeForArrayValue(values as any); + if (type && !checkTypeAssignable(preciseType, type, node)) { + return null; + } + return { valueKind: "ArrayValue", node: node, values: values as any, - type: createTypeForArrayValue(values as any), + type: type ?? preciseType, }; } @@ -3324,26 +3347,6 @@ export function createChecker(program: Program): Checker { }); } - function findTypesMatching(base: Type, constraint: Type): Type[] { - if (constraint.kind === base.kind) { - if (ignoreDiagnostics(isTypeAssignableTo(base, constraint, base))) { - return [constraint]; - } - return []; - } else if (constraint.kind === "Union") { - const matches: Type[] = []; - for (const variant of constraint.variants.values()) { - const subMatches = findTypesMatching(base, variant.type); - for (const match of subMatches) { - matches.push(match); - } - } - return matches; - } else { - return []; - } - } - function inferScalarForPrimitiveValue( base: Scalar, type: Type | undefined, @@ -3389,9 +3392,12 @@ export function createChecker(program: Program): Checker { function checkStringValue( literalType: StringLiteral | StringTemplate, - mapper: TypeMapper | undefined, - type: Type | undefined - ): StringValue { + type: Type | undefined, + node: Node + ): StringValue | null { + if (type && !checkTypeAssignable(literalType, type, node)) { + return null; + } let value: string; if (literalType.kind === "StringTemplate") { const [result, diagnostics] = stringTemplateToString(literalType); @@ -3409,7 +3415,14 @@ export function createChecker(program: Program): Checker { }; } - function checkNumericValue(literalType: NumericLiteral, type: Type | undefined): NumericValue { + function checkNumericValue( + literalType: NumericLiteral, + type: Type | undefined, + node: Node + ): NumericValue | null { + if (type && !checkTypeAssignable(literalType, type, node)) { + return null; + } const scalar = inferScalarForPrimitiveValue(getStdType("numeric"), type, literalType); return { valueKind: "NumericValue", @@ -3419,7 +3432,14 @@ export function createChecker(program: Program): Checker { }; } - function checkBooleanValue(literalType: BooleanLiteral, type: Type | undefined): BooleanValue { + function checkBooleanValue( + literalType: BooleanLiteral, + type: Type | undefined, + node: Node + ): BooleanValue | null { + if (type && !checkTypeAssignable(literalType, type, node)) { + return null; + } const scalar = inferScalarForPrimitiveValue(getStdType("boolean"), type, literalType); return { valueKind: "BooleanValue", @@ -3429,13 +3449,15 @@ export function createChecker(program: Program): Checker { }; } - function checkNullValue(literalType: NullType, type: Type | undefined): NullValue | null { - if ( - type !== undefined && - !ignoreDiagnostics(isTypeAssignableTo(literalType, type, literalType)) - ) { + function checkNullValue( + literalType: NullType, + type: Type | undefined, + node: Node + ): NullValue | null { + if (type && !checkTypeAssignable(literalType, type, node)) { return null; } + return { valueKind: "NullValue", type: type ?? literalType, @@ -3443,8 +3465,12 @@ export function createChecker(program: Program): Checker { }; } - function checkEnumValue(literalType: EnumMember, type: Type | undefined): EnumValue | null { - if (type !== undefined && !findTypesMatching(literalType, type)) { + function checkEnumValue( + literalType: EnumMember, + type: Type | undefined, + node: Node + ): EnumValue | null { + if (type && !checkTypeAssignable(literalType, type, node)) { return null; } return { diff --git a/packages/compiler/test/checker/values/const.test.ts b/packages/compiler/test/checker/values/const.test.ts index 11bf58c8b3..97633c08d7 100644 --- a/packages/compiler/test/checker/values/const.test.ts +++ b/packages/compiler/test/checker/values/const.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, it } from "vitest"; -import { compileValueType } from "./utils.js"; +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValueType, diagnoseUsage } from "./utils.js"; describe("without type it use the most precise type", () => { it.each([ @@ -10,6 +12,50 @@ describe("without type it use the most precise type", () => { [`#["abc"]`, "Tuple"], ])("%s => %s", async (input, kind) => { const value = await compileValueType("a", `const a = ${input};`); - expect(value.type.kind).toBe(kind); + strictEqual(value.type.kind, kind); + }); +}); + +it("when assigning another const it change the type", async () => { + const value = await compileValueType("b", `const a: int32 = 123;const b: int64 = a;`); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "int64"); +}); + +describe("invalid assignment", () => { + async function expectInvalidAssignment(code: string) { + const { diagnostics, pos, end } = await diagnoseUsage(code); + expectDiagnostics(diagnostics, { + code: "unassignable", + pos, + end, + }); + } + + describe("emit warning if assigning the wrong type", () => { + it("null", async () => { + await expectInvalidAssignment(`const a: int32 = ┆null┆;`); + }); + it("enum member", async () => { + await expectInvalidAssignment(` + const a: int32 = ┆Direction.up┆; + enum Direction { up, down }`); + }); + + it("string", async () => { + await expectInvalidAssignment(`const a: int32 = ┆"abc"┆;`); + }); + it("numeric", async () => { + await expectInvalidAssignment(`const a: string = ┆123┆;`); + }); + it("boolean", async () => { + await expectInvalidAssignment(`const a: string = ┆true┆;`); + }); + it("object value", async () => { + await expectInvalidAssignment(`const a: string = ┆#{ foo: "abc"}┆;`); + }); + it("array value", async () => { + await expectInvalidAssignment(`const a: string = ┆#["abc"]┆;`); + }); }); }); diff --git a/packages/compiler/test/checker/values/string-values.test.ts b/packages/compiler/test/checker/values/string-values.test.ts index 307e40f0e3..5e505bd07c 100644 --- a/packages/compiler/test/checker/values/string-values.test.ts +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -88,7 +88,7 @@ describe("validate literal are assignable", () => { `"abc"`, [ ["✔", `"abc"`], - ["✔", `"a\${"b"}c"`], + // ["✔", `"a\${"b"}c"`], // TODO: should that work? it doesn't in main [`✘`, `string("abc")`, `Type 'string' is not assignable to type '"abc"'`], ], ], diff --git a/packages/compiler/test/checker/values/utils.ts b/packages/compiler/test/checker/values/utils.ts index 8d798b691d..7e383956dc 100644 --- a/packages/compiler/test/checker/values/utils.ts +++ b/packages/compiler/test/checker/values/utils.ts @@ -9,11 +9,18 @@ import { export async function diagnoseUsage( code: string -): Promise<{ diagnostics: readonly Diagnostic[]; pos: number }> { +): Promise<{ diagnostics: readonly Diagnostic[]; pos: number; end?: number }> { const runner = await createTestRunner(); - const { source, pos } = extractCursor(code); + let end; + + let { source, pos } = extractCursor(code); + if (source.includes("┆")) { + const endMatch = extractCursor(source); + source = endMatch.source; + end = endMatch.pos; + } const diagnostics = await runner.diagnose(source); - return { diagnostics, pos }; + return { diagnostics, pos, end }; } export async function compileAndDiagnoseValueType( From 4662df370bf0177f4e300e791e4a1d9b8559216b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 19:45:16 -0700 Subject: [PATCH 067/184] Js marshaller conditional Numeric --- packages/compiler/src/core/checker.ts | 61 ++++++++++++------- packages/compiler/src/core/js-marshaller.ts | 58 +++++++++++++----- packages/compiler/src/core/types.ts | 4 +- .../compiler/test/checker/decorators.test.ts | 20 +++++- 4 files changed, 99 insertions(+), 44 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e777449233..2edd284eab 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4526,24 +4526,28 @@ export function createChecker(program: Program): Checker { : getIndexType( parameter.type.kind === "Value" ? parameter.type.target : parameter.type ); + if (restType) { + const perParamType = + parameter.type.kind === "Value" + ? ({ kind: "Value", target: restType } as const) + : restType; for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { - const arg = getTypeOrValueForNode(argNode, mapper, parameter.type); + const arg = getTypeOrValueForNode(argNode, mapper, perParamType); if ( arg !== null && !(isType(arg) && isErrorType(arg)) && - checkArgumentAssignable( - arg, - parameter.type.kind === "Value" ? { kind: "Value", target: restType } : restType, - argNode - ) + checkArgumentAssignable(arg, perParamType, argNode) ) { resolvedArgs.push({ value: arg, node: argNode, - jsValue: resolveDecoratorArgJsValue(arg, parameter.type.kind === "Value"), + jsValue: resolveDecoratorArgJsValue( + arg, + extractValueOfConstraints(parameter.type) + ), }); } else { hasError = true; @@ -4564,7 +4568,7 @@ export function createChecker(program: Program): Checker { resolvedArgs.push({ value: arg, node: argNode, - jsValue: resolveDecoratorArgJsValue(arg, parameter.type.kind === "Value"), + jsValue: resolveDecoratorArgJsValue(arg, extractValueOfConstraints(parameter.type)), }); } else { hasError = true; @@ -4578,10 +4582,10 @@ export function createChecker(program: Program): Checker { return type.kind === "Model" ? type.indexer?.value : undefined; } - function resolveDecoratorArgJsValue(value: Type | Value, valueOf: boolean) { - if (valueOf) { + function resolveDecoratorArgJsValue(value: Type | Value, valueConstraint: Type | undefined) { + if (valueConstraint !== undefined) { if (isValue(value) || value.kind === "Model" || value.kind === "Tuple") { - const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value); + const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value, valueConstraint); reportCheckerDiagnostics(diagnostics); return res ?? value; } else { @@ -7126,22 +7130,33 @@ function isAnonymous(type: Type) { return !("name" in type) || typeof type.name !== "string" || !type.name; } -const numericRanges: Record = { - int64: [Numeric("-9223372036854775808"), Numeric("9223372036854775807"), { int: true }], - int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true }], - int16: [Numeric("-32768"), Numeric("32767"), { int: true }], - int8: [Numeric("-128"), Numeric("127"), { int: true }], - uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true }], - uint32: [Numeric("0"), Numeric("4294967295"), { int: true }], - uint16: [Numeric("0"), Numeric("65535"), { int: true }], - uint8: [Numeric("0"), Numeric("255"), { int: true }], +export const numericRanges: Record< + string, + [min: Numeric, max: Numeric, options: { int: boolean; isJsNumber: boolean }] +> = { + int64: [ + Numeric("-9223372036854775808"), + Numeric("9223372036854775807"), + { int: true, isJsNumber: false }, + ], + int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true, isJsNumber: true }], + int16: [Numeric("-32768"), Numeric("32767"), { int: true, isJsNumber: true }], + int8: [Numeric("-128"), Numeric("127"), { int: true, isJsNumber: true }], + uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true, isJsNumber: true }], + uint32: [Numeric("0"), Numeric("4294967295"), { int: true, isJsNumber: true }], + uint16: [Numeric("0"), Numeric("65535"), { int: true, isJsNumber: true }], + uint8: [Numeric("0"), Numeric("255"), { int: true, isJsNumber: true }], safeint: [ Numeric(Number.MIN_SAFE_INTEGER.toString()), Numeric(Number.MAX_SAFE_INTEGER.toString()), - { int: true }, + { int: true, isJsNumber: true }, + ], + float32: [Numeric("-3.4e38"), Numeric("3.4e38"), { int: false, isJsNumber: true }], + float64: [ + Numeric(`${-Number.MAX_VALUE}`), + Numeric(Number.MAX_VALUE.toString()), + { int: false, isJsNumber: true }, ], - float32: [Numeric("-3.4e38"), Numeric("3.4e38"), { int: false }], - float64: [Numeric(`${-Number.MAX_VALUE}`), Numeric(Number.MAX_VALUE.toString()), { int: false }], }; /** diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 346e6c0377..6e470166e7 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,13 +1,15 @@ -import { isValue, typespecTypeToJson } from "./index.js"; +import { numericRanges } from "./checker.js"; +import { typespecTypeToJson } from "./decorator-utils.js"; +import { compilerAssert } from "./diagnostics.js"; +import { Numeric } from "./numeric.js"; +import { isValue } from "./type-utils.js"; import type { ArrayValue, - BooleanValue, Diagnostic, MarshalledValue, Model, NumericValue, ObjectValue, - StringValue, Tuple, Type, Value, @@ -15,27 +17,32 @@ import type { export function tryMarshallTypeForJS(type: T): MarshalledValue { if (isValue(type)) { - return marshallTypeForJS(type); + return marshallTypeForJS(type, undefined); } return type as any; } /** Legacy version that will cast models to object literals and tuple to tuple literals */ export function marshallTypeForJSWithLegacyCast( - type: T + entity: T, + valueConstraint: Type ): [MarshalledValue | undefined, readonly Diagnostic[]] { - if ("kind" in type) { - return typespecTypeToJson(type, type) as any; + if ("kind" in entity) { + return typespecTypeToJson(entity, entity) as any; } else { - return [marshallTypeForJS(type) as any, []]; + return [marshallTypeForJS(entity, valueConstraint) as any, []]; } } -export function marshallTypeForJS(type: T): MarshalledValue { +export function marshallTypeForJS( + type: T, + valueConstraint: Type | undefined +): MarshalledValue { switch (type.valueKind) { case "BooleanValue": case "StringValue": + return type.value as any; case "NumericValue": - return primitiveValueToJs(type) as any; + return numericValueToJs(type, valueConstraint) as any; case "ObjectValue": return objectValueToJs(type) as any; case "ArrayValue": @@ -49,19 +56,38 @@ export function marshallTypeForJS(type: T): MarshalledValue } } -function primitiveValueToJs( - type: T -): MarshalledValue { - return type.value as any; +function canNumericConstraintBeJsNumber(type: Type | undefined): boolean { + if (type === undefined) return true; + switch (type.kind) { + case "Scalar": + return numericRanges[type.name][2].isJsNumber; + case "Union": + return [...type.variants.values()].every((x) => canNumericConstraintBeJsNumber(x.type)); + default: + return true; + } +} + +function numericValueToJs(type: NumericValue, valueConstraint: Type | undefined): number | Numeric { + const canBeANumber = canNumericConstraintBeJsNumber(valueConstraint); + if (canBeANumber) { + const asNumber = type.value.asNumber(); + compilerAssert( + asNumber !== null, + `Numeric value '${type.value.toString()}' is not a able to convert to a number without loosing precision.` + ); + return asNumber; + } + return type.value; } function objectValueToJs(type: ObjectValue) { const result: Record = {}; for (const [key, value] of type.properties) { - result[key] = marshallTypeForJS(value.value); + result[key] = marshallTypeForJS(value.value, undefined); } return result; } function arrayValueToJs(type: ArrayValue) { - return type.values.map(marshallTypeForJS); + return type.values.map((x) => marshallTypeForJS(x, undefined)); } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index bad770be88..a5aca27a50 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -10,7 +10,7 @@ import type { TokenFlags } from "./scanner.js"; // prettier-ignore export type MarshalledValue = Value extends StringValue ? string - : Value extends NumericValue ? number + : Value extends NumericValue ? number | Numeric : Value extends BooleanValue ? boolean : Value extends ObjectValue ? Record : Value extends ArrayValue ? unknown[] @@ -30,7 +30,7 @@ export interface DecoratorArgument { /** * Marshalled value for use in Javascript. */ - jsValue: Type | Value | Record | unknown[] | string | number | boolean; + jsValue: Type | Value | Record | unknown[] | string | number | boolean | Numeric; node?: Node; } diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 1a313dbc3b..3fe189763a 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,6 +1,7 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { setTypeSpecNamespace } from "../../src/core/index.js"; +import { Numeric } from "../../src/core/numeric.js"; import { BasicTestRunner, TestHost, @@ -332,9 +333,22 @@ describe("compiler: checker: decorators", () => { }); describe("passing a numeric literal", () => { - it("valueof int32 cast the value to a JS number", async () => { - const arg = await testCallDecorator("valueof int32", `123`); - strictEqual(arg, 123); + it.each([ + ["int8", "number"], + ["uint8", "number"], + ["int16", "number"], + ["uint16", "number"], + ["int32", "number"], + ["uint32", "number"], + // Unsafe to convert to JS Number + ["int64", "Numeric"], + ])("valueof %s marshal to a %s", async (type, expectedKind) => { + const arg = await testCallDecorator(`valueof ${type}`, `123`); + if (expectedKind === "number") { + strictEqual(arg, 123); + } else { + deepStrictEqual(arg, Numeric("123")); + } }); }); From 1117fba4b1cada4a74fbe7b1152b7f7d980c494e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 20:03:46 -0700 Subject: [PATCH 068/184] More marshalling tests --- packages/compiler/src/core/checker.ts | 15 +++-- packages/compiler/src/core/js-marshaller.ts | 2 +- .../compiler/test/checker/decorators.test.ts | 58 ++++++++++++++----- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 2edd284eab..af04ebdb62 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -845,7 +845,6 @@ export function createChecker(program: Program): Checker { case SyntaxKind.UnknownKeyword: return unknownType; case SyntaxKind.ObjectLiteral: - console.trace("Checking", valueConstraint); return checkObjectValue(node, mapper, valueConstraint); case SyntaxKind.TupleLiteral: return checkArrayValue(node, mapper, valueConstraint); @@ -6773,7 +6772,7 @@ export function createChecker(program: Program): Checker { if (target.name === "float") return true; if (!(target.name in numericRanges)) return false; - const [low, high, options] = numericRanges[target.name]; + const [low, high, options] = numericRanges[target.name as keyof typeof numericRanges]; return source.gte(low) && source.lte(high) && (!options.int || isInt); } @@ -7130,10 +7129,7 @@ function isAnonymous(type: Type) { return !("name" in type) || typeof type.name !== "string" || !type.name; } -export const numericRanges: Record< - string, - [min: Numeric, max: Numeric, options: { int: boolean; isJsNumber: boolean }] -> = { +export const numericRanges = { int64: [ Numeric("-9223372036854775808"), Numeric("9223372036854775807"), @@ -7142,7 +7138,7 @@ export const numericRanges: Record< int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true, isJsNumber: true }], int16: [Numeric("-32768"), Numeric("32767"), { int: true, isJsNumber: true }], int8: [Numeric("-128"), Numeric("127"), { int: true, isJsNumber: true }], - uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true, isJsNumber: true }], + uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true, isJsNumber: false }], uint32: [Numeric("0"), Numeric("4294967295"), { int: true, isJsNumber: true }], uint16: [Numeric("0"), Numeric("65535"), { int: true, isJsNumber: true }], uint8: [Numeric("0"), Numeric("255"), { int: true, isJsNumber: true }], @@ -7157,7 +7153,10 @@ export const numericRanges: Record< Numeric(Number.MAX_VALUE.toString()), { int: false, isJsNumber: true }, ], -}; +} as const satisfies Record< + string, + [min: Numeric, max: Numeric, options: { int: boolean; isJsNumber: boolean }] +>; /** * Find all named models that could have been the source of the given diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 6e470166e7..79c0f08707 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -60,7 +60,7 @@ function canNumericConstraintBeJsNumber(type: Type | undefined): boolean { if (type === undefined) return true; switch (type.kind) { case "Scalar": - return numericRanges[type.name][2].isJsNumber; + return numericRanges[type.name as keyof typeof numericRanges]?.[2].isJsNumber; case "Union": return [...type.variants.values()].every((x) => canNumericConstraintBeJsNumber(x.type)); default: diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 3fe189763a..10f05fd9c8 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { setTypeSpecNamespace } from "../../src/core/index.js"; +import { numericRanges, setTypeSpecNamespace } from "../../src/core/index.js"; import { Numeric } from "../../src/core/numeric.js"; import { BasicTestRunner, @@ -333,23 +333,49 @@ describe("compiler: checker: decorators", () => { }); describe("passing a numeric literal", () => { - it.each([ - ["int8", "number"], - ["uint8", "number"], - ["int16", "number"], - ["uint16", "number"], - ["int32", "number"], - ["uint32", "number"], + const explicit: Required> = { + int8: "number", + uint8: "number", + int16: "number", + uint16: "number", + int32: "number", + uint32: "number", + safeint: "number", + float32: "number", + float64: "number", // Unsafe to convert to JS Number - ["int64", "Numeric"], - ])("valueof %s marshal to a %s", async (type, expectedKind) => { - const arg = await testCallDecorator(`valueof ${type}`, `123`); - if (expectedKind === "number") { - strictEqual(arg, 123); - } else { - deepStrictEqual(arg, Numeric("123")); + int64: "Numeric", + uint64: "Numeric", + }; + + const others = [ + ["integer", "Numeric"], + ["numeric", "Numeric"], + ["float", "Numeric"], + ["decimal", "Numeric"], + ["decimal128", "Numeric"], + + // Union of safe numeric + ["int8 | int16", "number", "int8(123)"], + + // Union of unsafe numeric + ["int64 | decimal128", "Numeric", "int8(123)"], + + // Union of safe and unsafe numeric + ["int64 | float64", "Numeric", "int8(123)"], + ]; + + it.each([...Object.entries(explicit), ...others])( + "valueof %s marshal to a %s", + async (type, expectedKind, cstr) => { + const arg = await testCallDecorator(`valueof ${type}`, cstr ?? `123`); + if (expectedKind === "number") { + strictEqual(arg, 123); + } else { + deepStrictEqual(arg, Numeric("123")); + } } - }); + ); }); describe("passing a boolean literal", () => { From f68a677a09b206afbc2a32cb54ffbc422d8eb694 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 20:13:25 -0700 Subject: [PATCH 069/184] nit fixes --- packages/compiler/test/checker/relation.test.ts | 2 +- packages/compiler/test/checker/values/numeric-values.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 92c461b71c..001b6124e8 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1191,7 +1191,7 @@ describe("compiler: checker: type relations", () => { { source: `123456`, target: "valueof int16" }, { code: "unassignable", - message: "Type '123456' is not assignable to type 'int16'", + message: "Type '123456' is not assignable to type 'valueof int16'", } ); }); diff --git a/packages/compiler/test/checker/values/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts index 0e92357063..54f47a10bb 100644 --- a/packages/compiler/test/checker/values/numeric-values.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -345,7 +345,7 @@ describe("custom numeric scalars", () => { strictEqual(value.value.asNumber(), 2); }); - it("validate value is valid using @minValue and @maxValue", async () => { + it.skip("validate value is valid using @minValue and @maxValue", async () => { const value = await compileValueType( `int4(2)`, `@minValue(0) @maxValue(15) scalar uint4 extends integer;` From e7854414be04df51226f9a213d09e0b26ea2050d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 21:15:14 -0700 Subject: [PATCH 070/184] special projection marshal --- packages/compiler/src/core/checker.ts | 21 ++++++++++++++++++--- packages/compiler/src/core/js-marshaller.ts | 8 -------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index af04ebdb62..927c98bd8e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -19,7 +19,7 @@ import { getTypeName, stringTemplateToString, } from "./helpers/index.js"; -import { marshallTypeForJSWithLegacyCast, tryMarshallTypeForJS } from "./js-marshaller.js"; +import { marshallTypeForJSWithLegacyCast } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { Numeric } from "./numeric.js"; import { @@ -6178,7 +6178,7 @@ export function createChecker(program: Program): Checker { if (!ref) throw new ProjectionError("Can't find decorator."); compilerAssert(ref.flags & SymbolFlags.Decorator, "should only resolve decorator symbols"); return createFunctionType((...args: Type[]): Type => { - ref.value!({ program }, ...args.map(tryMarshallTypeForJS)); + ref.value!({ program }, ...args.map(unsafe_projectionArgumentMarshalForJS)); return voidType; }); } @@ -6205,7 +6205,7 @@ export function createChecker(program: Program): Checker { } else if (ref.flags & SymbolFlags.Function) { // TODO: store this in a symbol link probably? const t: FunctionType = createFunctionType((...args: Type[]): Type => { - const retval = ref.value!(program, ...args.map(tryMarshallTypeForJS)); + const retval = ref.value!(program, ...args.map(unsafe_projectionArgumentMarshalForJS)); return marshalProjectionReturn(retval, { functionName: node.sv }); }); return t; @@ -7677,3 +7677,18 @@ const defaultSymbolResolutionOptions: SymbolResolutionOptions = { resolveDecorators: false, checkTemplateTypes: true, }; + +/** + * Convert LEGACY for projection. + * THIS IS BROKEN. Some decorators will not receive the correct type. + * It has been broken since the introduction of valueof. + * As projection as put on hold as long as versioning works we are in a good state. + */ +function unsafe_projectionArgumentMarshalForJS(arg: Type): any { + if (arg.kind === "Boolean" || arg.kind === "String" || arg.kind === "Number") { + return arg.value; + } else if (arg.kind === "StringTemplate") { + return stringTemplateToString(arg)[0]; + } + return arg as any; +} diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 79c0f08707..a72ebc4519 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -2,7 +2,6 @@ import { numericRanges } from "./checker.js"; import { typespecTypeToJson } from "./decorator-utils.js"; import { compilerAssert } from "./diagnostics.js"; import { Numeric } from "./numeric.js"; -import { isValue } from "./type-utils.js"; import type { ArrayValue, Diagnostic, @@ -15,13 +14,6 @@ import type { Value, } from "./types.js"; -export function tryMarshallTypeForJS(type: T): MarshalledValue { - if (isValue(type)) { - return marshallTypeForJS(type, undefined); - } - return type as any; -} - /** Legacy version that will cast models to object literals and tuple to tuple literals */ export function marshallTypeForJSWithLegacyCast( entity: T, From 10327729f8237533e03a7f6415402e64525771df Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 11 Apr 2024 21:30:21 -0700 Subject: [PATCH 071/184] small test fix --- packages/compiler/test/checker/relation.test.ts | 2 +- packages/compiler/test/parser.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 001b6124e8..3893178194 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -578,7 +578,7 @@ describe("compiler: checker: type relations", () => { { source: `3.4e40`, target: "float32" }, { code: "unassignable", - message: "Type '3.4e+40' is not assignable to type 'float32'", + message: "Type '3.4e40' is not assignable to type 'float32'", } ); }); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index cddb5b37b1..d34f03775c 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -170,7 +170,7 @@ describe("compiler: parser", () => { }`, ]); - parseErrorEach([["scalar uuid is string;", [{ message: "';', or '{' expected." }]]]); + parseErrorEach([["scalar uuid is string;", [{ message: "'{' expected." }]]]); }); describe("interface statements", () => { From aef40ad98f25f28630725d8dc9e86fd1d5e7c391 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 07:43:38 -0700 Subject: [PATCH 072/184] Convert numeric decorators --- packages/compiler/generated-defs/TypeSpec.ts | 17 ++-- packages/compiler/src/core/index.ts | 1 + packages/compiler/src/core/type-utils.ts | 20 ++-- packages/compiler/src/lib/decorators.ts | 97 ++++++++++++++----- .../test/decorators/range-limits.test.ts | 6 +- .../decorators-signatures.ts | 11 ++- .../tspd/src/ref-doc/emitters/markdown.ts | 10 +- .../tspd/src/ref-doc/utils/type-signature.ts | 2 + 8 files changed, 105 insertions(+), 59 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 87118e44b3..eb92bd45eb 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -5,6 +5,7 @@ import type { Model, ModelProperty, Namespace, + Numeric, Operation, Scalar, Type, @@ -254,7 +255,7 @@ export type PatternDecorator = ( export type MinLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -270,7 +271,7 @@ export type MinLengthDecorator = ( export type MaxLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -286,7 +287,7 @@ export type MaxLengthDecorator = ( export type MinItemsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -302,7 +303,7 @@ export type MinItemsDecorator = ( export type MaxItemsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -318,7 +319,7 @@ export type MaxItemsDecorator = ( export type MinValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -334,7 +335,7 @@ export type MinValueDecorator = ( export type MaxValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -351,7 +352,7 @@ export type MaxValueDecorator = ( export type MinValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** @@ -368,7 +369,7 @@ export type MinValueExclusiveDecorator = ( export type MaxValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index 2b4eebfda6..02ae5ad72c 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -19,6 +19,7 @@ export { } from "./library.js"; export * from "./module-resolver.js"; export { NodeHost } from "./node-host.js"; +export { Numeric, isNumeric } from "./numeric.js"; export * from "./options.js"; export { getPositionBeforeTrivia } from "./parser-utils.js"; export * from "./parser.js"; diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index 76b263b8b3..39eeb5c5e8 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -22,24 +22,24 @@ import { VoidType, } from "./types.js"; -export function isErrorType(type: Type): type is ErrorType { - return type.kind === "Intrinsic" && type.name === "ErrorType"; +export function isErrorType(type: Entity): type is ErrorType { + return "kind" in type && type.kind === "Intrinsic" && type.name === "ErrorType"; } -export function isVoidType(type: Type): type is VoidType { - return type.kind === "Intrinsic" && type.name === "void"; +export function isVoidType(type: Entity): type is VoidType { + return "kind" in type && type.kind === "Intrinsic" && type.name === "void"; } -export function isNeverType(type: Type): type is NeverType { - return type.kind === "Intrinsic" && type.name === "never"; +export function isNeverType(type: Entity): type is NeverType { + return "kind" in type && type.kind === "Intrinsic" && type.name === "never"; } -export function isUnknownType(type: Type): type is UnknownType { - return type.kind === "Intrinsic" && type.name === "unknown"; +export function isUnknownType(type: Entity): type is UnknownType { + return "kind" in type && type.kind === "Intrinsic" && type.name === "unknown"; } -export function isNullType(type: Type): type is NullType { - return type.kind === "Intrinsic" && type.name === "null"; +export function isNullType(type: Entity): type is NullType { + return "kind" in type && type.kind === "Intrinsic" && type.name === "null"; } export function isType(entity: Entity): entity is Type { diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index d8b31012b9..c339f1112f 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -43,6 +43,7 @@ import { } from "../core/decorator-utils.js"; import { getDeprecationDetails, markDeprecated } from "../core/deprecation.js"; import { + Numeric, StdTypeName, getDiscriminatedUnion, getTypeName, @@ -528,13 +529,13 @@ const minLengthValuesKey = createStateSymbol("minLengthValues"); export const $minLength: MinLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - minLength: number + minLength: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $minLength); if ( !validateTargetingAString(context, target, "@minLength") || - !validateRange(context, minLength, getMaxLength(context.program, target)) + !validateRange(context, minLength, getMaxLengthAsNumeric(context.program, target)) ) { return; } @@ -542,10 +543,19 @@ export const $minLength: MinLengthDecorator = ( context.program.stateMap(minLengthValuesKey).set(target, minLength); }; -export function getMinLength(program: Program, target: Type): number | undefined { +/** + * Get the minimum length of a string type as a {@link Numeric} value. + * @param program Current program + * @param target Type with the `@minLength` decorator + */ +export function getMinLengthAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(minLengthValuesKey).get(target); } +export function getMinLength(program: Program, target: Type): number | undefined { + return getMinLengthAsNumeric(program, target)?.asNumber() ?? undefined; +} + // -- @maxLength decorator --------------------- const maxLengthValuesKey = createStateSymbol("maxLengthValues"); @@ -553,13 +563,13 @@ const maxLengthValuesKey = createStateSymbol("maxLengthValues"); export const $maxLength: MaxLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - maxLength: number + maxLength: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $maxLength); if ( !validateTargetingAString(context, target, "@maxLength") || - !validateRange(context, getMinLength(context.program, target), maxLength) + !validateRange(context, getMinLengthAsNumeric(context.program, target), maxLength) ) { return; } @@ -568,6 +578,16 @@ export const $maxLength: MaxLengthDecorator = ( }; export function getMaxLength(program: Program, target: Type): number | undefined { + return getMaxLengthAsNumeric(program, target)?.asNumber() ?? undefined; +} + +// TODO: better name? +/** + * Get the maximum length of a string type as a {@link Numeric} value. + * @param program Current program + * @param target Type with the `@maxLength` decorator + */ +export function getMaxLengthAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(maxLengthValuesKey).get(target); } @@ -578,7 +598,7 @@ const minItemsValuesKey = createStateSymbol("minItems"); export const $minItems: MinItemsDecorator = ( context: DecoratorContext, target: Type, - minItems: number + minItems: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $minItems); @@ -593,17 +613,21 @@ export const $minItems: MinItemsDecorator = ( }); } - if (!validateRange(context, minItems, getMaxItems(context.program, target))) { + if (!validateRange(context, minItems, getMaxItemsAsNumeric(context.program, target))) { return; } context.program.stateMap(minItemsValuesKey).set(target, minItems); }; -export function getMinItems(program: Program, target: Type): number | undefined { +export function getMinItemsAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(minItemsValuesKey).get(target); } +export function getMinItems(program: Program, target: Type): number | undefined { + return getMinItemsAsNumeric(program, target)?.asNumber() ?? undefined; +} + // -- @maxLength decorator --------------------- const maxItemsValuesKey = createStateSymbol("maxItems"); @@ -611,7 +635,7 @@ const maxItemsValuesKey = createStateSymbol("maxItems"); export const $maxItems: MaxItemsDecorator = ( context: DecoratorContext, target: Type, - maxItems: number + maxItems: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $maxItems); @@ -625,17 +649,21 @@ export const $maxItems: MaxItemsDecorator = ( target: context.decoratorTarget, }); } - if (!validateRange(context, getMinItems(context.program, target), maxItems)) { + if (!validateRange(context, getMinItemsAsNumeric(context.program, target), maxItems)) { return; } context.program.stateMap(maxItemsValuesKey).set(target, maxItems); }; -export function getMaxItems(program: Program, target: Type): number | undefined { +export function getMaxItemsAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(maxItemsValuesKey).get(target); } +export function getMaxItems(program: Program, target: Type): number | undefined { + return getMaxItemsAsNumeric(program, target)?.asNumber() ?? undefined; +} + // -- @minValue decorator --------------------- const minValuesKey = createStateSymbol("minValues"); @@ -643,7 +671,7 @@ const minValuesKey = createStateSymbol("minValues"); export const $minValue: MinValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - minValue: number + minValue: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $minValue); validateDecoratorNotOnType(context, target, $minValueExclusive, $minValue); @@ -657,7 +685,8 @@ export const $minValue: MinValueDecorator = ( !validateRange( context, minValue, - getMaxValue(context.program, target) ?? getMaxValueExclusive(context.program, target) + getMaxValueAsNumeric(context.program, target) ?? + getMaxValueExclusiveAsNumeric(context.program, target) ) ) { return; @@ -665,9 +694,12 @@ export const $minValue: MinValueDecorator = ( program.stateMap(minValuesKey).set(target, minValue); }; -export function getMinValue(program: Program, target: Type): number | undefined { +export function getMinValueAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(minValuesKey).get(target); } +export function getMinValue(program: Program, target: Type): number | undefined { + return getMinValueAsNumeric(program, target)?.asNumber() ?? undefined; +} // -- @maxValue decorator --------------------- @@ -676,7 +708,7 @@ const maxValuesKey = createStateSymbol("maxValues"); export const $maxValue: MaxValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - maxValue: number + maxValue: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $maxValue); validateDecoratorNotOnType(context, target, $maxValueExclusive, $maxValue); @@ -688,7 +720,8 @@ export const $maxValue: MaxValueDecorator = ( if ( !validateRange( context, - getMinValue(context.program, target) ?? getMinValueExclusive(context.program, target), + getMinValueAsNumeric(context.program, target) ?? + getMinValueExclusiveAsNumeric(context.program, target), maxValue ) ) { @@ -697,9 +730,12 @@ export const $maxValue: MaxValueDecorator = ( program.stateMap(maxValuesKey).set(target, maxValue); }; -export function getMaxValue(program: Program, target: Type): number | undefined { +export function getMaxValueAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(maxValuesKey).get(target); } +export function getMaxValue(program: Program, target: Type): number | undefined { + return getMaxValueAsNumeric(program, target)?.asNumber() ?? undefined; +} // -- @minValueExclusive decorator --------------------- @@ -708,7 +744,7 @@ const minValueExclusiveKey = createStateSymbol("minValueExclusive"); export const $minValueExclusive: MinValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - minValueExclusive: number + minValueExclusive: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $minValueExclusive); validateDecoratorNotOnType(context, target, $minValue, $minValueExclusive); @@ -722,7 +758,8 @@ export const $minValueExclusive: MinValueExclusiveDecorator = ( !validateRange( context, minValueExclusive, - getMaxValue(context.program, target) ?? getMaxValueExclusive(context.program, target) + getMaxValueAsNumeric(context.program, target) ?? + getMaxValueExclusiveAsNumeric(context.program, target) ) ) { return; @@ -730,9 +767,12 @@ export const $minValueExclusive: MinValueExclusiveDecorator = ( program.stateMap(minValueExclusiveKey).set(target, minValueExclusive); }; -export function getMinValueExclusive(program: Program, target: Type): number | undefined { +export function getMinValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(minValueExclusiveKey).get(target); } +export function getMinValueExclusive(program: Program, target: Type): number | undefined { + return getMinValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined; +} // -- @maxValueExclusive decorator --------------------- @@ -741,7 +781,7 @@ const maxValueExclusiveKey = createStateSymbol("maxValueExclusive"); export const $maxValueExclusive: MaxValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - maxValueExclusive: number + maxValueExclusive: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $maxValueExclusive); validateDecoratorNotOnType(context, target, $maxValue, $maxValueExclusive); @@ -753,7 +793,8 @@ export const $maxValueExclusive: MaxValueExclusiveDecorator = ( if ( !validateRange( context, - getMinValue(context.program, target) ?? getMinValueExclusive(context.program, target), + getMinValueAsNumeric(context.program, target) ?? + getMinValueExclusiveAsNumeric(context.program, target), maxValueExclusive ) ) { @@ -762,9 +803,12 @@ export const $maxValueExclusive: MaxValueExclusiveDecorator = ( program.stateMap(maxValueExclusiveKey).set(target, maxValueExclusive); }; -export function getMaxValueExclusive(program: Program, target: Type): number | undefined { +export function getMaxValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(maxValueExclusiveKey).get(target); } +export function getMaxValueExclusive(program: Program, target: Type): number | undefined { + return getMaxValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined; +} // -- @secret decorator --------------------- @@ -1441,14 +1485,15 @@ export function hasProjectedName(program: Program, target: Type, projectionName: function validateRange( context: DecoratorContext, - min: number | undefined, - max: number | undefined + min: Numeric | undefined, + max: Numeric | undefined ): boolean { if (min === undefined || max === undefined) { return true; } + if (min.gt(max)) { + console.log("Min", min, max); - if (min > max) { reportDiagnostic(context.program, { code: "invalid-range", format: { start: min.toString(), end: max.toString() }, diff --git a/packages/compiler/test/decorators/range-limits.test.ts b/packages/compiler/test/decorators/range-limits.test.ts index db9cf248ec..64402bdc20 100644 --- a/packages/compiler/test/decorators/range-limits.test.ts +++ b/packages/compiler/test/decorators/range-limits.test.ts @@ -33,7 +33,7 @@ describe("compiler: range limiting decorators", () => { }); describe("@minValue, @maxValue", () => { - it("applies @minLength and @maxLength decorators on ints", async () => { + it("applies on ints", async () => { const { Foo } = (await runner.compile(` @test model Foo { @minValue(2) @@ -47,7 +47,7 @@ describe("compiler: range limiting decorators", () => { strictEqual(getMaxValue(runner.program, floorProp), 10); }); - it("applies @minLength and @maxLength decorators on float", async () => { + it("applies on float", async () => { const { Foo } = (await runner.compile(` @test model Foo { @minValue(2.5) @@ -61,7 +61,7 @@ describe("compiler: range limiting decorators", () => { strictEqual(getMaxValue(runner.program, percentProp), 32.9); }); - it("applies @minLength and @maxLength decorators on nullable numeric", async () => { + it("applies on nullable numeric", async () => { const { Foo } = (await runner.compile(` @test model Foo { @minValue(2.5) diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index 98df43e0a2..f852f09bdc 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -194,21 +194,22 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat function getStdScalarTSType(scalar: Scalar & { name: IntrinsicScalarName }): string { switch (scalar.name) { case "numeric": + case "decimal": + case "decimal128": + case "float": case "integer": + case "int64": + case "uint64": + return useCompilerType("Numeric"); case "int8": case "int16": case "int32": - case "int64": case "safeint": case "uint8": case "uint16": case "uint32": - case "uint64": - case "float": case "float64": case "float32": - case "decimal": - case "decimal128": return "number"; case "string": case "url": diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts index 25ca7a48b7..955657c0a3 100644 --- a/packages/tspd/src/ref-doc/emitters/markdown.ts +++ b/packages/tspd/src/ref-doc/emitters/markdown.ts @@ -1,4 +1,4 @@ -import { Entity, getEntityName, isValue, resolvePath } from "@typespec/compiler"; +import { Entity, getEntityName, isType, resolvePath } from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -188,11 +188,7 @@ export class MarkdownRenderer { } ref(type: Entity): string { - const namedType = - type.kind !== "Value" && - type.kind !== "ParamConstraintUnion" && - !isValue(type) && - this.refDoc.getNamedTypeRefDoc(type); + const namedType = isType(type) && this.refDoc.getNamedTypeRefDoc(type); if (namedType) { return link( inlinecode(namedType.name), @@ -201,7 +197,7 @@ export class MarkdownRenderer { } // So we don't show (anonymous model) until this gets improved. - if (type.kind === "Model" && type.name === "" && type.properties.size > 0) { + if ("kind" in type && type.kind === "Model" && type.name === "" && type.properties.size > 0) { return inlinecode("{...}"); } return inlinecode( diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 04095a9f85..4499238a2c 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -50,6 +50,8 @@ export function getTypeSignature(type: Type | ValueType): string { return `(intrinsic) ${type.name}`; case "FunctionParameter": return getFunctionParameterSignature(type); + case "ScalarConstructor": + return `(scalar constructor) ${getTypeName(type)}`; case "StringTemplate": return `(string template)\n${getStringTemplateSignature(type)}`; case "StringTemplateSpan": From bde6748a98bb4f8280744107db34c5bc9bffbfea Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 08:50:01 -0700 Subject: [PATCH 073/184] Show contextual errors for arguments --- packages/compiler/src/core/checker.ts | 206 +++++++++++------- .../test/checker/values/scalar-values.test.ts | 12 +- 2 files changed, 133 insertions(+), 85 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 927c98bd8e..2a10a3def3 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5,12 +5,7 @@ import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-iden import { createModelToLiteralCodeFix } from "./compiler-code-fixes/model-to-literal.codefix.js"; import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; -import { - ProjectionError, - compilerAssert, - ignoreDiagnostics, - reportDeprecated, -} from "./diagnostics.js"; +import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { TypeNameOptions, @@ -687,12 +682,16 @@ export function createChecker(program: Program): Checker { return typeOrValue; } - function getValueForNode(node: Node, mapper?: TypeMapper, constraint?: Type): Value | null { + function getValueForNode( + node: Node, + mapper?: TypeMapper, + constraint?: CheckValueConstraint + ): Value | null { let entity = getTypeOrValueForNodeInternal(node, mapper, constraint); if (entity === null || isValue(entity)) { return entity; } - entity = tryUsingValueOfType(entity, mapper, constraint, node); + entity = tryUsingValueOfType(entity, constraint, node); if (entity === null || isValue(entity)) { return entity; } @@ -708,16 +707,9 @@ export function createChecker(program: Program): Checker { function tryUsingValueOfType( type: Type, - mapper: TypeMapper | undefined, - constraint: Type | undefined, + constraint: CheckValueConstraint | undefined, node: Node ): Type | Value | null { - if ( - constraint !== undefined && - !ignoreDiagnostics(isTypeAssignableTo(type, type, constraint)) - ) { - return null; - } switch (type.kind) { case "String": case "StringTemplate": @@ -738,6 +730,15 @@ export function createChecker(program: Program): Checker { return type; } } + + interface CheckConstraint { + kind: "argument" | "assignment"; + constraint: Type | ValueType | ParamConstraintUnion; + } + interface CheckValueConstraint { + kind: "argument" | "assignment"; + type: Type; + } /** * Gets a type or value depending on the node and current constraint. * For nodes that can be both type or values(e.g. string), the value will be returned if the constraint expect a value of that type even if the constrain also allows the type. @@ -746,7 +747,7 @@ export function createChecker(program: Program): Checker { function getTypeOrValueForNode( node: Node, mapper?: TypeMapper, - constraint?: Type | ValueType | ParamConstraintUnion | undefined + constraint?: CheckConstraint | undefined ): Type | Value | null { const valueConstraint = extractValueOfConstraints(constraint); const entity = getTypeOrValueForNodeInternal(node, mapper, valueConstraint); @@ -755,7 +756,7 @@ export function createChecker(program: Program): Checker { } if (valueConstraint) { - return tryUsingValueOfType(entity, mapper, valueConstraint, node); + return tryUsingValueOfType(entity, valueConstraint, node); } return entity; @@ -763,18 +764,18 @@ export function createChecker(program: Program): Checker { /** Extact the type constraint a value should match. */ function extractValueOfConstraints( - constraint: Type | ValueType | ParamConstraintUnion | undefined - ): Type | undefined { - if (constraint === undefined || isType(constraint)) { + constraint: CheckConstraint | undefined + ): CheckValueConstraint | undefined { + if (constraint === undefined || isType(constraint.constraint)) { return undefined; } - if (constraint.kind === "Value") { - return constraint.target; + if (constraint.constraint.kind === "Value") { + return { kind: constraint.kind, type: constraint.constraint.target }; } else { - const valueOfOptions = constraint.options + const valueOfOptions = constraint.constraint.options .filter((x): x is ValueType => x.kind === "Value") .map((x) => x.target); - return createUnion(valueOfOptions); + return { kind: constraint.kind, type: createUnion(valueOfOptions) }; } } @@ -782,7 +783,7 @@ export function createChecker(program: Program): Checker { function getTypeOrValueForNodeInternal( node: Node, mapper?: TypeMapper, - valueConstraint?: Type | undefined + valueConstraint?: CheckValueConstraint | undefined ): Type | Value | null { switch (node.kind) { case SyntaxKind.ModelExpression: @@ -3225,18 +3226,18 @@ export function createChecker(program: Program): Checker { function checkObjectValue( node: ObjectLiteralNode, mapper: TypeMapper | undefined, - type: Type | undefined + constraint: CheckValueConstraint | undefined ): ObjectValue | null { const properties = checkObjectLiteralProperties(node, mapper); const preciseType = createTypeForObjectValue(properties); - if (type && !checkTypeAssignable(preciseType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(preciseType, constraint, node)) { return null; } return { valueKind: "ObjectValue", node: node, properties, - type: type ?? preciseType, + type: constraint ? constraint.type : preciseType, }; } @@ -3311,7 +3312,7 @@ export function createChecker(program: Program): Checker { function checkArrayValue( node: TupleLiteralNode, mapper: TypeMapper | undefined, - type: Type | undefined + constraint: CheckValueConstraint | undefined ): ArrayValue | null { let hasError = false; const values = node.values.map((itemNode) => { @@ -3326,7 +3327,7 @@ export function createChecker(program: Program): Checker { } const preciseType = createTypeForArrayValue(values as any); - if (type && !checkTypeAssignable(preciseType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(preciseType, constraint, node)) { return null; } @@ -3334,7 +3335,7 @@ export function createChecker(program: Program): Checker { valueKind: "ArrayValue", node: node, values: values as any, - type: type ?? preciseType, + type: constraint ? constraint.type : preciseType, }; } @@ -3391,10 +3392,10 @@ export function createChecker(program: Program): Checker { function checkStringValue( literalType: StringLiteral | StringTemplate, - type: Type | undefined, + constraint: CheckValueConstraint | undefined, node: Node ): StringValue | null { - if (type && !checkTypeAssignable(literalType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } let value: string; @@ -3405,100 +3406,92 @@ export function createChecker(program: Program): Checker { } else { value = literalType.value; } - const scalar = inferScalarForPrimitiveValue(getStdType("string"), type, literalType); + const scalar = inferScalarForPrimitiveValue( + getStdType("string"), + constraint?.type, + literalType + ); return { valueKind: "StringValue", value, - type: type ?? literalType, + type: constraint ? constraint.type : literalType, scalar, }; } function checkNumericValue( literalType: NumericLiteral, - type: Type | undefined, + constraint: CheckValueConstraint | undefined, node: Node ): NumericValue | null { - if (type && !checkTypeAssignable(literalType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } - const scalar = inferScalarForPrimitiveValue(getStdType("numeric"), type, literalType); + const scalar = inferScalarForPrimitiveValue( + getStdType("numeric"), + constraint?.type, + literalType + ); return { valueKind: "NumericValue", value: Numeric(literalType.valueAsString), - type: literalType, + type: constraint ? constraint.type : literalType, scalar, }; } function checkBooleanValue( literalType: BooleanLiteral, - type: Type | undefined, + constraint: CheckValueConstraint | undefined, node: Node ): BooleanValue | null { - if (type && !checkTypeAssignable(literalType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } - const scalar = inferScalarForPrimitiveValue(getStdType("boolean"), type, literalType); + const scalar = inferScalarForPrimitiveValue( + getStdType("boolean"), + constraint?.type, + literalType + ); return { valueKind: "BooleanValue", value: literalType.value, - type: type ?? literalType, + type: constraint ? constraint.type : literalType, scalar, }; } function checkNullValue( literalType: NullType, - type: Type | undefined, + constraint: CheckValueConstraint | undefined, node: Node ): NullValue | null { - if (type && !checkTypeAssignable(literalType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } return { valueKind: "NullValue", - type: type ?? literalType, + type: constraint ? constraint.type : literalType, value: null, }; } function checkEnumValue( literalType: EnumMember, - type: Type | undefined, + constraint: CheckValueConstraint | undefined, node: Node ): EnumValue | null { - if (type && !checkTypeAssignable(literalType, type, node)) { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } return { valueKind: "EnumValue", - type: type ?? literalType, + type: constraint ? constraint.type : literalType, value: literalType, }; } - /** - * Check and resolve a type for the given type reference node. - * @param node Node. - * @param mapper Type mapper for template instantiation context. - * @param instantiateTemplate If templated type should be instantiated if they haven't yet. - * @returns Resolved type. - */ - function checkValueReference( - node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined - ): Value | null { - const sym = resolveTypeReferenceSym(node, mapper); - if (!sym) { - return null; - } - - const value = checkValueReferenceSymbol(sym, node, mapper); - return value; - } - function checkCallExpressionTarget( node: CallExpressionNode, mapper: TypeMapper | undefined @@ -3610,7 +3603,7 @@ export function createChecker(program: Program): Checker { for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { - const arg = getValueForNode(argNode, mapper, restType); + const arg = getValueForNode(argNode, mapper, { kind: "argument", type: restType }); if (arg === null) { hasError = true; continue; @@ -3627,7 +3620,10 @@ export function createChecker(program: Program): Checker { } const argNode = node.arguments[index]; if (argNode) { - const arg = getValueForNode(argNode, mapper, parameter.type as any); // TODO: change if we change this to not be a FunctionParameter + const arg = getValueForNode(argNode, mapper, { + kind: "argument", + type: parameter.type as any, // TODO: change if we change this to not be a FunctionParameter + }); if (arg === null) { hasError = true; continue; @@ -4534,7 +4530,10 @@ export function createChecker(program: Program): Checker { for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { - const arg = getTypeOrValueForNode(argNode, mapper, perParamType); + const arg = getTypeOrValueForNode(argNode, mapper, { + kind: "argument", + constraint: perParamType, + }); if ( arg !== null && !(isType(arg) && isErrorType(arg)) && @@ -4545,7 +4544,10 @@ export function createChecker(program: Program): Checker { node: argNode, jsValue: resolveDecoratorArgJsValue( arg, - extractValueOfConstraints(parameter.type) + extractValueOfConstraints({ + kind: "argument", + constraint: parameter.type, + }) ), }); } else { @@ -4558,7 +4560,10 @@ export function createChecker(program: Program): Checker { } const argNode = node.arguments[index]; if (argNode) { - const arg = getTypeOrValueForNode(argNode, mapper, parameter.type); + const arg = getTypeOrValueForNode(argNode, mapper, { + kind: "argument", + constraint: parameter.type, + }); if ( arg !== null && !(isType(arg) && isErrorType(arg)) && @@ -4567,7 +4572,13 @@ export function createChecker(program: Program): Checker { resolvedArgs.push({ value: arg, node: argNode, - jsValue: resolveDecoratorArgJsValue(arg, extractValueOfConstraints(parameter.type)), + jsValue: resolveDecoratorArgJsValue( + arg, + extractValueOfConstraints({ + kind: "argument", + constraint: parameter.type, + }) + ), }); } else { hasError = true; @@ -4581,10 +4592,13 @@ export function createChecker(program: Program): Checker { return type.kind === "Model" ? type.indexer?.value : undefined; } - function resolveDecoratorArgJsValue(value: Type | Value, valueConstraint: Type | undefined) { + function resolveDecoratorArgJsValue( + value: Type | Value, + valueConstraint: CheckValueConstraint | undefined + ) { if (valueConstraint !== undefined) { if (isValue(value) || value.kind === "Model" || value.kind === "Tuple") { - const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value, valueConstraint); + const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value, valueConstraint.type); reportCheckerDiagnostics(diagnostics); return res ?? value; } else { @@ -4858,7 +4872,7 @@ export function createChecker(program: Program): Checker { } pendingResolutions.start(symId, ResolutionKind.Value); - const value = getValueForNode(node.value, undefined, type); + const value = getValueForNode(node.value, undefined, type && { kind: "assignment", type }); pendingResolutions.finish(symId, ResolutionKind.Value); if (value === null || (type && !checkValueOfType(value, type, node.id))) { links.value = null; @@ -6308,6 +6322,40 @@ export function createChecker(program: Program): Checker { return parts.reverse().join("."); } + /** + * Check if the source type can be assigned to the target type and emit diagnostics + * @param source Type of a value + * @param constraint + * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. + */ + function checkTypeOfValueMatchConstraint( + source: Entity, + constraint: CheckValueConstraint, + diagnosticTarget: DiagnosticTarget + ): boolean { + if (constraint.type === undefined) { + console.trace("Ab"); + } + const [related, diagnostics] = isTypeAssignableTo(source, constraint.type, diagnosticTarget); + if (!related) { + if (constraint.kind === "argument") { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument", + format: { + value: getEntityName(source), + expected: getEntityName(constraint.type), + }, + target: diagnosticTarget, + }) + ); + } else { + reportCheckerDiagnostics(diagnostics); + } + } + return related; + } + /** * Check if the source type can be assigned to the target type and emit diagnostics * @param source Source type diff --git a/packages/compiler/test/checker/values/scalar-values.test.ts b/packages/compiler/test/checker/values/scalar-values.test.ts index 459992278e..d1787ad060 100644 --- a/packages/compiler/test/checker/values/scalar-values.test.ts +++ b/packages/compiler/test/checker/values/scalar-values.test.ts @@ -72,8 +72,8 @@ describe("instantiate with named constructor", () => { it("emit warning if passing wrong type to constructor", async () => { const diagnostics = await diagnoseValueType(`ipv4.fromString(123)`, ipv4Code); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '123' is not assignable to type 'string'", + code: "invalid-argument", + message: "Argument '123' is not assignable to parameter of type 'string'", }); }); @@ -131,8 +131,8 @@ describe("instantiate with named constructor", () => { ` ); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '123' is not assignable to type 'string'", + code: "invalid-argument", + message: "Argument '123' is not assignable to parameter of type 'string'", }); }); }); @@ -175,8 +175,8 @@ describe("instantiate with named constructor", () => { ` ); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '123' is not assignable to type 'string'", + code: "invalid-argument", + message: "Argument '123' is not assignable to parameter of type 'string'", }); }); }); From 6a30fd79a068bba99e36de42b0887b275d20d23f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 09:09:29 -0700 Subject: [PATCH 074/184] Fix template parameters --- packages/compiler/src/core/checker.ts | 19 +++++++++++++------ .../test/decorators/decorators.test.ts | 10 +++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 2a10a3def3..04a414b570 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1161,8 +1161,8 @@ export function createChecker(program: Program): Checker { checkArgument: (() => [Node, Type | Value]) | null; } const initMap = new Map( - decls.map(function (decl) { - const declaredType = getTypeOrValueForNode(decl)! as TemplateParameter; + decls.map((decl) => { + const declaredType = getTypeForNode(decl)! as TemplateParameter; positional.push(declaredType); params.set(decl.id.sv, declaredType); @@ -1180,8 +1180,15 @@ export function createChecker(program: Program): Checker { let named = false; for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { - function deferredCheck(): [Node, Type | Value] { - return [arg, getTypeOrValueForNode(arg.argument, mapper) ?? errorType]; + function deferredCheck(param: TemplateParameter): () => [Node, Type | Value] { + return () => [ + arg, + getTypeOrValueForNode( + arg.argument, + mapper, + param.constraint && { kind: "argument", constraint: param.constraint } + ) ?? errorType, + ]; } if (arg.name) { @@ -1217,7 +1224,7 @@ export function createChecker(program: Program): Checker { continue; } - initMap.get(param)!.checkArgument = deferredCheck; + initMap.get(param)!.checkArgument = deferredCheck(param); } else { if (named) { reportCheckerDiagnostic( @@ -1243,7 +1250,7 @@ export function createChecker(program: Program): Checker { const param = positional[idx]; - initMap.get(param)!.checkArgument ??= deferredCheck; + initMap.get(param)!.checkArgument ??= deferredCheck(param); } } diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index a4ea14661e..2e700fee4d 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -222,7 +222,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, + message: `Argument '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -267,7 +267,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, + message: `Argument '123' is not assignable to parameter of type 'string'`, }); }); @@ -320,7 +320,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, + message: `Argument '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -347,7 +347,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, + message: `Argument '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -520,7 +520,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '4' is not assignable to parameter of type 'valueof string'", + message: "Argument '4' is not assignable to parameter of type 'string'", }, ]); }); From a759d23d4f22fa387a7d365b09c9883845cf242a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 09:16:56 -0700 Subject: [PATCH 075/184] . --- packages/compiler/test/projection/projection-logic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/test/projection/projection-logic.test.ts b/packages/compiler/test/projection/projection-logic.test.ts index d9f67125b6..f390037133 100644 --- a/packages/compiler/test/projection/projection-logic.test.ts +++ b/packages/compiler/test/projection/projection-logic.test.ts @@ -237,7 +237,7 @@ describe("compiler: projections: logic", () => { return p.stateMap(addedOnKey).get(t) || -1; }, getRemovedOn(p: Program, t: Type) { - return p.stateMap(removedOnKey).get(t) || Infinity; + return p.stateMap(removedOnKey).get(t) || Number.MAX_SAFE_INTEGER; }, getRenamedFromVersions(p: Program, t: Type) { return p.stateMap(renamedFromKey).get(t)?.v ?? -1; From a6aeb718c732844f070448332589253e2002d1d9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 09:54:01 -0700 Subject: [PATCH 076/184] progress --- packages/compiler/src/core/checker.ts | 48 ++++++++++++++----- packages/compiler/test/checker/model.test.ts | 11 +++-- packages/compiler/test/checker/scalar.test.ts | 2 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 04a414b570..7d6eb12102 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4285,7 +4285,13 @@ export function createChecker(program: Program): Checker { } else { pendingResolutions.start(symId, ResolutionKind.Type); type.type = getTypeForNode(prop.value, mapper); - type.default = prop.default && (checkDefault(prop.default, type.type) as any); // TODO: fix; + if (prop.default) { + const defaultValue = checkDefaultValue(prop.default, type.type); + if (defaultValue !== null) { + type.defaultValue = defaultValue; + type.default = checkLegacyDefault(prop.default); + } + } if (links) { linkType(links, type, mapper); } @@ -4323,6 +4329,7 @@ export function createChecker(program: Program): Checker { }; } + // TODO: remove? function isDefaultValue(type: Type | Value): boolean { if (isType(type)) { if (type.kind === "UnionVariant") { @@ -4347,31 +4354,46 @@ export function createChecker(program: Program): Checker { return isValue(type); } - function checkDefault(defaultNode: Node, type: Type): Type | Value { - const defaultType = getTypeOrValueForNode(defaultNode, undefined); - if (defaultType === null || isErrorType(type)) { - return errorType; + function checkDefaultValue(defaultNode: Node, type: Type): Value | null { + if (isErrorType(type)) { + // if the prop type is an error we don't need to validate again. + return null; } - if (!isDefaultValue(defaultType)) { + const defaultValue = getValueForNode(defaultNode, undefined, { + kind: "assignment", + type, + }); + if (defaultValue === null) { + return null; + } + if (!isDefaultValue(defaultValue)) { reportCheckerDiagnostic( createDiagnostic({ code: "unsupported-default", // TODO: fix this - format: { type: (defaultType as any).kind }, + format: { type: (defaultValue as any).kind }, target: defaultNode, }) ); - return errorType; + return null; } - const [related, diagnostics] = isValue(defaultType) - ? isValueOfType(defaultType, type, defaultNode) - : isTypeAssignableTo(defaultType, type, defaultNode); + const [related, diagnostics] = isValueOfType(defaultValue, type, defaultNode); if (!related) { reportCheckerDiagnostics(diagnostics); - return errorType; + return null; } else { - return defaultType; + return defaultValue; + } + } + /** Fill in the legacy `.default` property. + * We do do checking here we just keep existing behavior. + */ + function checkLegacyDefault(defaultNode: Node): Type | undefined { + const resolved = getTypeOrValueForNode(defaultNode, undefined); + if (resolved === null || !isType(resolved)) { + return undefined; } + return resolved; } function checkDecorator( diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 886e7ebbba..5e8f64cbe2 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -1,5 +1,5 @@ import { deepStrictEqual, match, ok, strictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { isTemplateDeclaration } from "../../src/core/type-utils.js"; import { Model, ModelProperty, Type } from "../../src/core/types.js"; import { Operation, getDoc, isArrayModelType, isRecordModelType } from "../../src/index.js"; @@ -103,6 +103,7 @@ describe("compiler: models", () => { ]); }); + // TODO: update to test new defaultValue describe("assign default values", () => { const testCases: [string, string, any][] = [ ["boolean", `false`, { kind: "Boolean", value: false, isFinished: false }], @@ -121,7 +122,7 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - deepStrictEqual({ ...foo.default }, expectedValue); + expect({ ...foo.default }).toMatchObject(expectedValue); }); } @@ -183,13 +184,13 @@ describe("compiler: models", () => { testHost.addTypeSpecFile("main.tsp", source); const diagnostics = await testHost.diagnose("main.tsp"); expectDiagnostics(diagnostics, { - code: "unsupported-default", - message: "Default must be have a value type but has type 'TemplateParameter'.", + code: "expect-value", + message: "D refers to a type, but is being used as a value here.", pos, }); }); - it(`doesn't emit unsupported-default diagnostic when type is an error`, async () => { + it(`doesn't emit additional diagnostic when type is an error`, async () => { testHost.addTypeSpecFile( "main.tsp", ` diff --git a/packages/compiler/test/checker/scalar.test.ts b/packages/compiler/test/checker/scalar.test.ts index cc707a7e49..10caf9e1e0 100644 --- a/packages/compiler/test/checker/scalar.test.ts +++ b/packages/compiler/test/checker/scalar.test.ts @@ -89,7 +89,7 @@ describe("compiler: scalars", () => { ok(p); expectIdenticalTypes(p.type, S); strictEqual(p.defaultValue?.valueKind, "NumericValue"); - strictEqual(p.defaultValue.value, 42); + strictEqual(p.defaultValue.value.asNumber(), 42); }); it("allows custom boolean scalar to have a default value", async () => { From edd8ad0454121c7ee78fd41340afb163b2a9b574 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 10:22:53 -0700 Subject: [PATCH 077/184] progress --- packages/compiler/src/core/checker.ts | 4 +- .../compiler/test/checker/relation.test.ts | 46 +++++++++---------- packages/html-program-viewer/src/ui.tsx | 1 + 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 7d6eb12102..fc37e85d16 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -273,7 +273,7 @@ export interface Checker { resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]]; /** @internal */ - getTypeOrValueForNode(node: Node): Type | Value | null; + getValueForNode(node: Node): Value | null; errorType: ErrorType; voidType: VoidType; @@ -430,7 +430,7 @@ export function createChecker(program: Program): Checker { isStdType, getStdType, resolveTypeReference, - getTypeOrValueForNode, + getValueForNode, }; const projectionMembers = createProjectionMembers(checker); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 3893178194..a91be69909 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1,7 +1,7 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { - AliasStatementNode, + ConstStatementNode, Diagnostic, FunctionParameterNode, Model, @@ -74,14 +74,14 @@ describe("compiler: checker: type relations", () => { ${commonCode ?? ""} extern dec mock(target: unknown, target: ${target}); - alias Source = ${cursor}${source}; + const Source = ${cursor}${source}; `); await runner.compile(code); - const alias: AliasStatementNode | undefined = runner.program.sourceFiles + const constStatement = runner.program.sourceFiles .get(resolveVirtualPath("main.tsp")) - ?.statements.find((x): x is AliasStatementNode => x.kind === SyntaxKind.AliasStatement); - ok(alias); - const sourceProp = runner.program.checker.getTypeOrValueForNode(alias.value); + ?.statements.find((x): x is ConstStatementNode => x.kind === SyntaxKind.ConstStatement); + ok(constStatement); + const sourceProp = runner.program.checker.getValueForNode(constStatement.value); ok(sourceProp, `Could not find source type for ${source}`); const decDeclaration = runner.program .getGlobalNamespaceType() @@ -92,7 +92,7 @@ describe("compiler: checker: type relations", () => { const [related, diagnostics] = runner.program.checker.isTypeAssignableTo( sourceProp, targetProp, - alias.value + constStatement.value ); return { related, diagnostics, expectedDiagnosticPos: pos }; } @@ -463,9 +463,9 @@ describe("compiler: checker: type relations", () => { await expectTypeAssignable({ source: "int64", target: "int64" }); }); - it("can assign numeric literal between -9223372036854775807 and 9223372036854775808", async () => { - await expectTypeAssignable({ source: "-9223372036854775807", target: "int64" }); - await expectTypeAssignable({ source: "9223372036854775808", target: "int64" }); + it("can assign numeric literal between -9223372036854775808 and 9223372036854775807", async () => { + await expectTypeAssignable({ source: "-9223372036854775808", target: "int64" }); + await expectTypeAssignable({ source: "9223372036854775807", target: "int64" }); }); it("emit diagnostic when numeric literal is out of range large", async () => { @@ -1127,11 +1127,11 @@ describe("compiler: checker: type relations", () => { describe("Value constraint", () => { describe("valueof string", () => { it("can assign string literal", async () => { - await expectTypeAssignable({ source: `"foo bar"`, target: "valueof string" }); + await checkValueAssignable({ source: `"foo bar"`, target: "string" }); }); it("cannot assign numeric literal", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `123`, target: "valueof string" }, { code: "unassignable", @@ -1153,11 +1153,11 @@ describe("compiler: checker: type relations", () => { describe("valueof boolean", () => { it("can assign boolean literal", async () => { - await expectTypeAssignable({ source: `true`, target: "valueof boolean" }); + await expectValueAssignable({ source: `true`, target: "valueof boolean" }); }); it("cannot assign numeric literal", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `123`, target: "valueof boolean" }, { code: "unassignable", @@ -1179,7 +1179,7 @@ describe("compiler: checker: type relations", () => { describe("valueof int16", () => { it("can assign int16 literal", async () => { - await expectTypeAssignable({ source: `12`, target: "valueof int16" }); + await expectValueAssignable({ source: `12`, target: "valueof int16" }); }); it("can assign valueof int8", async () => { @@ -1187,17 +1187,17 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign int too large", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `123456`, target: "valueof int16" }, { code: "unassignable", - message: "Type '123456' is not assignable to type 'valueof int16'", + message: "Type '123456' is not assignable to type 'int16'", } ); }); it("cannot assign float", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `12.6`, target: "valueof int16" }, { code: "unassignable", @@ -1207,7 +1207,7 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign string literal", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `"foo bar"`, target: "valueof int16" }, { code: "unassignable", @@ -1229,11 +1229,11 @@ describe("compiler: checker: type relations", () => { describe("valueof float32", () => { it("can assign float32 literal", async () => { - await expectTypeAssignable({ source: `12.6`, target: "valueof float32" }); + await expectValueAssignable({ source: `12.6`, target: "valueof float32" }); }); it("cannot assign string literal", async () => { - await expectTypeNotAssignable( + await expectValueNotAssignable( { source: `"foo bar"`, target: "valueof float32" }, { code: "unassignable", @@ -1518,14 +1518,14 @@ describe("compiler: checker: type relations", () => { describe("can assign types or values when constraint accept both", () => { it.each([ - ["{}", "(valueof unknown) | unknown"], ["#{}", "(valueof unknown) | unknown"], - ["{}", "(valueof {}) | {}"], ["#{}", "(valueof {}) | {}"], ])(`%s => %s`, async (source, target) => { await expectValueAssignable({ source, target }); }); it.each([ + ["{}", "(valueof unknown) | unknown"], + ["{}", "(valueof {}) | {}"], ["(valueof {}) | {}", "(valueof {}) | {} | (valueof []) | []"], ["(valueof {}) | {}", "(valueof {}) | {}"], ])(`%s => %s`, async (source, target) => { diff --git a/packages/html-program-viewer/src/ui.tsx b/packages/html-program-viewer/src/ui.tsx index 15a7981dd0..98ada85894 100644 --- a/packages/html-program-viewer/src/ui.tsx +++ b/packages/html-program-viewer/src/ui.tsx @@ -267,6 +267,7 @@ const ScalarUI: FunctionComponent<{ type: Scalar }> = ({ type }) => { properties={{ baseScalar: "ref", derivedScalars: "ref", + constructors: "nested", }} /> ); From a57ff401f06e3e5e8c63c206d50535a54905b095 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 12:48:08 -0700 Subject: [PATCH 078/184] Improve diag messages and fix tests --- packages/compiler/src/core/checker.ts | 57 ++++--------------- .../src/core/helpers/type-name-utils.ts | 5 +- .../compiler/test/checker/relation.test.ts | 17 +++--- .../test/checker/values/array-values.test.ts | 2 +- .../test/checker/values/object-values.test.ts | 4 +- .../test/decorators/decorators.test.ts | 2 +- .../compiler/test/decorators/service.test.ts | 5 +- packages/http/test/http-decorators.test.ts | 11 ++-- 8 files changed, 33 insertions(+), 70 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index a8eba09f46..ae0c3652ed 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -6583,12 +6583,17 @@ export function createChecker(program: Program): Checker { isArrayModelType(program, target) && source.kind === "Model" ) { - return hasIndexAndIsAssignableTo( - source, - target as Model & { indexer: ModelIndexer }, - diagnosticTarget, - relationCache - ); + if (isArrayModelType(program, source)) { + return hasIndexAndIsAssignableTo( + source, + target as Model & { indexer: ModelIndexer }, + diagnosticTarget, + relationCache + ); + } else { + // For other models just fallback to unassignable + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + } } else if (target.kind === "Model" && source.kind === "Model") { return isModelRelatedTo(source, target, diagnosticTarget, relationCache); } else if ( @@ -6720,46 +6725,6 @@ export function createChecker(program: Program): Checker { relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { return isTypeAssignableToInternal(source.type, target, diagnosticTarget, relationCache); - // TODO: cleanup - // if (isUnknownType(target)) return [Related.true, []]; - // if (source.valueKind === "NullValue") { - // return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); - // } - // if (target.kind === "Union") { - // for (const option of target.variants.values()) { - // const [related] = isValueOfTypeInternal( - // source, - // option.type, - // diagnosticTarget, - // relationCache - // ); - // if (related) { - // return [Related.true, []]; - // } - // } - // } - - // switch (source.valueKind) { - // case "ObjectValue": - // if (target.kind === "Model" && !isArrayModelType(program, target)) { - // return isObjectLiteralOfModelType(source, target, diagnosticTarget, relationCache); - // } - // break; - // case "ArrayValue": - // if (target.kind === "Model" && isArrayModelType(program, target)) { - // return isTupleLiteralOfArrayType(source, target, diagnosticTarget, relationCache); - // } else if (target.kind === "Tuple") { - // return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); - // } - // break; - // case "StringValue": - // case "NumericValue": - // case "BooleanValue": - // case "EnumMemberValue": - // return isTypeAssignableToInternal(source, target, diagnosticTarget, relationCache); - // } - - // return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } function isReflectionType(type: Type): type is Model & { name: ReflectionTypeName } { diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 5dfefda137..652100c1af 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -158,7 +158,10 @@ function getModelName(model: Model, options: TypeNameOptions | undefined) { } if (model.name === "") { - return nsPrefix + "(anonymous model)"; + return ( + nsPrefix + + `{ ${[...model.properties.values()].map((prop) => `${prop.name}: ${getTypeName(prop.type, options)}`).join(", ")} }` + ); } const modelName = nsPrefix + getIdentifierName(model.name, options); if (isTemplateInstance(model)) { diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 986db5180a..e1911147fc 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -800,8 +800,7 @@ describe("compiler: checker: type relations", () => { { source: `{foo: "abc"}`, target: `{foo: string, bar: string}` }, { code: "missing-property", - message: - "Property 'bar' is missing on type '(anonymous model)' but required in '(anonymous model)'", + message: `Property 'bar' is missing on type '{ foo: "abc" }' but required in '{ foo: string, bar: string }'`, } ); }); @@ -897,8 +896,8 @@ describe("compiler: checker: type relations", () => { await expectTypeNotAssignable( { source: `{}`, target: "string[]" }, { - code: "missing-index", - message: "Index signature for type 'integer' is missing in type '{}'.", + code: "unassignable", + message: "Type '{}' is not assignable to type 'string[]'", } ); }); @@ -1066,7 +1065,7 @@ describe("compiler: checker: type relations", () => { expectDiagnostics(diagnostics, { code: "missing-property", - message: `Property 'a' is missing on type '(anonymous model)' but required in '(anonymous model)'`, + message: `Property 'a' is missing on type '{ b: string }' but required in '{ a: string }'`, }); }); @@ -1337,7 +1336,7 @@ describe("compiler: checker: type relations", () => { }, { code: "unassignable", - message: `Type '#["foo"]' is not assignable to type 'Info'`, + message: `Type '["foo"]' is not assignable to type 'Info'`, } ); }); @@ -1398,7 +1397,7 @@ describe("compiler: checker: type relations", () => { }, { code: "unassignable", - message: `Type '#{name: "foo"}' is not assignable to type 'string[]'`, + message: `Type '{ name: "foo" }' is not assignable to type 'string[]'`, } ); }); @@ -1431,7 +1430,7 @@ describe("compiler: checker: type relations", () => { { code: "unassignable", message: [ - `Type '#["foo"]' is not assignable to type '[string, string]'`, + `Type '["foo"]' is not assignable to type '[string, string]'`, " Source has 1 element(s) but target requires 2.", ].join("\n"), } @@ -1447,7 +1446,7 @@ describe("compiler: checker: type relations", () => { { code: "unassignable", message: [ - `Type '#["a", "b", "c"]' is not assignable to type '[string, string]'`, + `Type '["a", "b", "c"]' is not assignable to type '[string, string]'`, " Source has 3 element(s) but target requires 2.", ].join("\n"), } diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index 8d7507a040..fa6c6814ca 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -53,7 +53,7 @@ it("emit diagnostic if referencing a non literal type", async () => { const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); expectDiagnostics(diagnostics, { code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", + message: "{ thisIsAModel: true } refers to a type, but is being used as a value here.", }); }); diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index 463423b4ca..83f64c11c3 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -81,7 +81,7 @@ describe("spreading", () => { ); expectDiagnostics(diagnostics, { code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", + message: `{ name: "John" } refers to a type, but is being used as a value here.`, }); }); @@ -118,7 +118,7 @@ it("emit diagnostic if referencing a non literal type", async () => { const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); expectDiagnostics(diagnostics, { code: "expect-value", - message: "(anonymous model) refers to a type, but is being used as a value here.", + message: "{ thisIsAModel: true } refers to a type, but is being used as a value here.", }); }); diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 2e700fee4d..2d44b3ad5d 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -857,7 +857,7 @@ describe("compiler: built-in decorators", () => { { code: "missing-property", message: - "Property 'param' is missing on type '(anonymous model)' but required in '(anonymous model)'", + "Property 'param' is missing on type '{ foo: boolean }' but required in '{ param: string | int32 }'", severity: "error", }, { diff --git a/packages/compiler/test/decorators/service.test.ts b/packages/compiler/test/decorators/service.test.ts index 5b62eb4b92..8e36458415 100644 --- a/packages/compiler/test/decorators/service.test.ts +++ b/packages/compiler/test/decorators/service.test.ts @@ -64,8 +64,7 @@ describe("compiler: service", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: - "Argument '(anonymous model)' is not assignable to parameter of type 'ServiceOptions'", + message: "Argument '{ title: 123 }' is not assignable to parameter of type 'ServiceOptions'", }); }); @@ -103,7 +102,7 @@ describe("compiler: service", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", message: - "Argument '(anonymous model)' is not assignable to parameter of type 'ServiceOptions'", + "Argument '{ version: 123 }' is not assignable to parameter of type 'ServiceOptions'", }); }); }); diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 590814b9fb..81cc1c916b 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -85,12 +85,11 @@ describe("http: decorators", () => { { code: "invalid-argument", message: - "Argument '(anonymous model)' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", + "Argument '{ name: 123 } is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", }, { code: "invalid-argument", - message: - "Argument '(anonymous model)' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", + message: `Argument '{ format: "invalid" }' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'`, }, ]); }); @@ -173,13 +172,11 @@ describe("http: decorators", () => { }, { code: "invalid-argument", - message: - "Argument '(anonymous model)' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'", + message: `Argument '{name: 123}' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'`, }, { code: "invalid-argument", - message: - "Argument '(anonymous model)' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'", + message: `Argument '{ format: "invalid" }' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'`, }, ]); }); From 1a4f18d7eba786443c5f63a2fd5f05631cbc436c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:27:04 -0700 Subject: [PATCH 079/184] Validate excess properties for value declarations --- packages/compiler/src/core/checker.ts | 164 +++++------------- packages/compiler/src/core/types.ts | 9 +- packages/compiler/src/lib/decorators.ts | 2 - .../compiler/test/checker/relation.test.ts | 47 +++-- 4 files changed, 78 insertions(+), 144 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index ae0c3652ed..5493bf0935 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -39,7 +39,6 @@ import { import { AliasStatementNode, ArrayExpressionNode, - ArrayModelType, ArrayValue, AugmentDecoratorStatementNode, BooleanLiteral, @@ -1323,24 +1322,6 @@ export function createChecker(program: Program): Checker { return finalMap; } - function checkValueReferenceSymbol( - sym: Sym, - node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined - ): Value | null { - // TODO: use common checkTypeOrValueReferenceSymbol - if (sym.flags & SymbolFlags.Const) { - return getValueForNode(sym.declarations[0], mapper); - } - reportCheckerDiagnostic( - createDiagnostic({ - code: "expect-value", - format: { name: sym.name }, - target: node, - }) - ); - return null; - } /** * Check and resolve the type for the given symbol + node. * @param sym Symbol @@ -3236,7 +3217,7 @@ export function createChecker(program: Program): Checker { constraint: CheckValueConstraint | undefined ): ObjectValue | null { const properties = checkObjectLiteralProperties(node, mapper); - const preciseType = createTypeForObjectValue(properties); + const preciseType = createTypeForObjectValue(node, properties); if (constraint && !checkTypeOfValueMatchConstraint(preciseType, constraint, node)) { return null; } @@ -3248,27 +3229,33 @@ export function createChecker(program: Program): Checker { }; } - function createTypeForObjectValue(properties: Map): Model { - return createAndFinishType({ + function createTypeForObjectValue( + node: ObjectLiteralNode, + properties: Map + ): Model { + const model = createType({ kind: "Model", name: "", - properties: createRekeyableMap( - [...properties.entries()].map(([name, prop]) => [ - name, - createModelPropertyForObjectPropertyDescriptor(prop), - ]) - ), + node, + properties: createRekeyableMap(), decorators: [], derivedModels: [], }); + + for (const prop of properties.values()) { + model.properties.set(prop.name, createModelPropertyForObjectPropertyDescriptor(prop, model)); + } + return finishType(model); } function createModelPropertyForObjectPropertyDescriptor( - prop: ObjectValuePropertyDescriptor + prop: ObjectValuePropertyDescriptor, + parentModel: Model ): ModelProperty { return createAndFinishType({ kind: "ModelProperty", - node: undefined!, + node: prop.node, + model: parentModel, optional: false, name: prop.name, type: prop.value.type, @@ -3286,13 +3273,13 @@ export function createChecker(program: Program): Checker { if ("id" in prop) { const value = getValueForNode(prop.value, mapper); if (value !== null) { - properties.set(prop.id.sv, { name: prop.id.sv, value: value }); + properties.set(prop.id.sv, { name: prop.id.sv, value: value, node: prop }); } } else { const targetType = checkObjectSpreadProperty(prop.target, mapper); if (targetType) { for (const [name, value] of targetType.properties) { - properties.set(name, value); + properties.set(name, { ...value }); } } } @@ -3333,7 +3320,7 @@ export function createChecker(program: Program): Checker { return null; } - const preciseType = createTypeForArrayValue(values as any); + const preciseType = createTypeForArrayValue(node, values as any); if (constraint && !checkTypeOfValueMatchConstraint(preciseType, constraint, node)) { return null; } @@ -3346,10 +3333,10 @@ export function createChecker(program: Program): Checker { }; } - function createTypeForArrayValue(values: Value[]): Tuple { + function createTypeForArrayValue(node: TupleLiteralNode, values: Value[]): Tuple { return createAndFinishType({ kind: "Tuple", - node: undefined!, + node, values: values.map((x) => x.type), }); } @@ -4335,7 +4322,7 @@ export function createChecker(program: Program): Checker { if (type.kind === "UnionVariant") { return isValue(type.type); } - if (type.kind === "Tuple") { + if (type.kind === "Tuple" && type.node.kind === SyntaxKind.TupleExpression) { reportCheckerDiagnostic( createDiagnostic({ code: "deprecated", @@ -6362,9 +6349,6 @@ export function createChecker(program: Program): Checker { constraint: CheckValueConstraint, diagnosticTarget: DiagnosticTarget ): boolean { - if (constraint.type === undefined) { - console.trace("Ab"); - } const [related, diagnostics] = isTypeAssignableTo(source, constraint.type, diagnosticTarget); if (!related) { if (constraint.kind === "argument") { @@ -6644,6 +6628,7 @@ export function createChecker(program: Program): Checker { if ( isSourceAType && source.kind === "Tuple" && + source.node.kind === SyntaxKind.TupleExpression && isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === Related.true ) { @@ -6836,6 +6821,7 @@ export function createChecker(program: Program): Checker { const sourceProperty = getProperty(source, prop.name); if (sourceProperty === undefined) { if (!prop.optional) { + console.trace("HERer"); diagnostics.push( createDiagnostic({ code: "missing-property", @@ -6884,107 +6870,37 @@ export function createChecker(program: Program): Checker { diagnostics.push(...indexDiagnostics); } } - } - - return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; - } - - function isObjectLiteralOfModelType( - source: ObjectValue, - target: Model, - diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { - relationCache.set([source, target], Related.maybe); - const diagnostics: Diagnostic[] = []; - const remainingProperties = new Map(source.properties); - for (const prop of walkPropertiesInherited(target)) { - const sourceProperty = source.properties.get(prop.name); - if (sourceProperty === undefined) { - if (!prop.optional) { + } else if (shouldCheckExcessProperties(source)) { + for (const [propName, prop] of remainingProperties) { + if (shouldCheckExcessProperty(prop)) { diagnostics.push( createDiagnostic({ - code: "missing-property", + code: "unexpected-property", format: { - propertyName: prop.name, - sourceType: getEntityName(source), - targetType: getEntityName(target), + propertyName: propName, + type: getEntityName(target), }, - target: source, + target: prop, }) ); } - } else { - remainingProperties.delete(prop.name); - const [related, propDiagnostics] = isValueOfTypeInternal( - sourceProperty.value, - prop.type, - diagnosticTarget, - relationCache - ); - if (!related) { - diagnostics.push(...propDiagnostics); - } } } - if (target.indexer) { - const [_, indexerDiagnostics] = arePropertiesAssignableToIndexer( - remainingProperties as any, // TODO: fix - target.indexer.value, - diagnosticTarget, - relationCache - ); - diagnostics.push(...indexerDiagnostics); - } else { - for (const [propName] of remainingProperties) { - diagnostics.push( - createDiagnostic({ - code: "unexpected-property", - format: { - propertyName: propName, - type: getEntityName(target), - }, - target: getObjectLiteralPropertyNode(source, propName), - }) - ); - } - } return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; } - function getObjectLiteralPropertyNode( - object: ObjectValue, - propertyName: string - ): DiagnosticTarget { + + /** If we should check for excess properties on the given model. */ + function shouldCheckExcessProperties(model: Model) { + return model.node?.kind === SyntaxKind.ObjectLiteral; + } + /** If we should check for this specific property */ + function shouldCheckExcessProperty(prop: ModelProperty) { return ( - object.node.properties.find( - (x) => x.kind === SyntaxKind.ObjectLiteralProperty && x.id.sv === propertyName - ) ?? object.node + prop.node?.kind === SyntaxKind.ObjectLiteralProperty && prop.node.parent === prop.model?.node ); } - function isTupleLiteralOfArrayType( - source: ArrayValue, - target: ArrayModelType, - diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Entity, Entity], Related> - ): [Related, readonly Diagnostic[]] { - relationCache.set([source, target], Related.maybe); - for (const value of source.values) { - const [related, diagnostics] = isValueOfTypeInternal( - value, - target.indexer.value, - diagnosticTarget, - relationCache - ); - if (!related) { - return [Related.false, diagnostics]; - } - } - - return [Related.true, []]; - } - function getProperty(model: Model, name: string): ModelProperty | undefined { return ( model.properties.get(name) ?? diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index a5aca27a50..671b084f1e 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -247,7 +247,8 @@ export interface Model extends BaseType, DecoratedType, TemplatedTypeBase { | ModelStatementNode | ModelExpressionNode | IntersectionExpressionNode - | ProjectionModelExpressionNode; + | ProjectionModelExpressionNode + | ObjectLiteralNode; namespace?: Namespace; indexer?: ModelIndexer; @@ -293,7 +294,8 @@ export interface ModelProperty extends BaseType, DecoratedType { | ModelPropertyNode | ModelSpreadPropertyNode | ProjectionModelPropertyNode - | ProjectionModelSpreadPropertyNode; + | ProjectionModelSpreadPropertyNode + | ObjectLiteralPropertyNode; name: string; type: Type; // when spread or intersection operators make new property types, @@ -328,6 +330,7 @@ export interface ObjectValue extends BaseValue { } export interface ObjectValuePropertyDescriptor { + node: ObjectLiteralPropertyNode; name: string; value: Value; } @@ -596,7 +599,7 @@ export interface StringTemplateSpanValue extends BaseType { export interface Tuple extends BaseType { kind: "Tuple"; - node: TupleExpressionNode; + node: TupleExpressionNode | TupleLiteralNode; values: Type[]; } diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index c339f1112f..59ae7bf766 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -1492,8 +1492,6 @@ function validateRange( return true; } if (min.gt(max)) { - console.log("Min", min, max); - reportDiagnostic(context.program, { code: "invalid-range", format: { start: min.toString(), end: max.toString() }, diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index e1911147fc..f7cce6f560 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -76,13 +76,17 @@ describe("compiler: checker: type relations", () => { const Source = ${cursor}${source}; `); + + console.log("Code", code); await runner.compile(code); const constStatement = runner.program.sourceFiles .get(resolveVirtualPath("main.tsp")) - ?.statements.find((x): x is ConstStatementNode => x.kind === SyntaxKind.ConstStatement); + ?.statements.find( + (x): x is ConstStatementNode => x.kind === SyntaxKind.ConstStatement && x.id.sv === "Source" + ); ok(constStatement); - const sourceProp = runner.program.checker.getValueForNode(constStatement.value); - ok(sourceProp, `Could not find source type for ${source}`); + const value = runner.program.checker.getValueForNode(constStatement.value); + ok(value, `Could not find source type for ${source}`); const decDeclaration = runner.program .getGlobalNamespaceType() .decoratorDeclarations.get("mock"); @@ -90,7 +94,7 @@ describe("compiler: checker: type relations", () => { ok(targetProp, `Could not find target type for ${target}`); const [related, diagnostics] = runner.program.checker.isTypeAssignableTo( - sourceProp, + value, targetProp, constStatement.value ); @@ -1313,18 +1317,31 @@ describe("compiler: checker: type relations", () => { ); }); - it("emit diagnostic when using extra properties", async () => { - await expectValueNotAssignable( - { - source: `#{name: "foo", ┆notDefined: "bar"}`, + describe("excess properties", () => { + it("emit diagnostic when using extra properties", async () => { + await expectValueNotAssignable( + { + source: `#{name: "foo", ┆notDefined: "bar"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }, + { + code: "unexpected-property", + message: `Object literal may only specify known properties, and 'notDefined' does not exist in type 'Info'.`, + } + ); + }); + + it("don't emit diagnostic when the extra props are spread into it", async () => { + await expectValueAssignable({ + source: `#{name: "foo", ...common}`, target: "valueof Info", - commonCode: `model Info { name: string }`, - }, - { - code: "unexpected-property", - message: `Object literal may only specify known properties, and 'notDefined' does not exist in type 'Info'.`, - } - ); + commonCode: ` + const common = #{notDefined: "bar"}; + model Info { name: string } + `, + }); + }); }); it("cannot assign a tuple literal", async () => { From deed56643f7dd381d76ac78b3157667bc5bdbef9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:27:27 -0700 Subject: [PATCH 080/184] cleanup --- packages/compiler/src/core/checker.ts | 1 - packages/compiler/test/checker/relation.test.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5493bf0935..e65b09b31f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -6821,7 +6821,6 @@ export function createChecker(program: Program): Checker { const sourceProperty = getProperty(source, prop.name); if (sourceProperty === undefined) { if (!prop.optional) { - console.trace("HERer"); diagnostics.push( createDiagnostic({ code: "missing-property", diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index f7cce6f560..8b21b79c03 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -77,7 +77,6 @@ describe("compiler: checker: type relations", () => { const Source = ${cursor}${source}; `); - console.log("Code", code); await runner.compile(code); const constStatement = runner.program.sourceFiles .get(resolveVirtualPath("main.tsp")) From 603dd2c87067067005cd1085c7ec0e681af7eb85 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:33:07 -0700 Subject: [PATCH 081/184] cleanup and fix last tests --- packages/compiler/src/core/checker.ts | 36 ------------------- .../compiler/test/checker/relation.test.ts | 2 +- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e65b09b31f..1baf0c0c70 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4316,31 +4316,6 @@ export function createChecker(program: Program): Checker { }; } - // TODO: remove? - function isDefaultValue(type: Type | Value): boolean { - if (isType(type)) { - if (type.kind === "UnionVariant") { - return isValue(type.type); - } - if (type.kind === "Tuple" && type.node.kind === SyntaxKind.TupleExpression) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "deprecated", - codefixes: [createTupleToLiteralCodeFix(type.node)], - format: { - message: - "Using a tuple as a default value is deprecated. Use a tuple literal instead. `#[]`", - }, - target: type.node, - }) - ); - return true; - } - } - - return isValue(type); - } - function checkDefaultValue(defaultNode: Node, type: Type): Value | null { if (isErrorType(type)) { // if the prop type is an error we don't need to validate again. @@ -4353,17 +4328,6 @@ export function createChecker(program: Program): Checker { if (defaultValue === null) { return null; } - if (!isDefaultValue(defaultValue)) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "unsupported-default", - // TODO: fix this - format: { type: (defaultValue as any).kind }, - target: defaultNode, - }) - ); - return null; - } const [related, diagnostics] = isValueOfType(defaultValue, type, defaultNode); if (!related) { reportCheckerDiagnostics(diagnostics); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 8b21b79c03..3215564ad8 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1385,7 +1385,7 @@ describe("compiler: checker: type relations", () => { }); it("can assign a tuple (LEGACY)", async () => { - await expectValueAssignable({ + await expectTypeAssignable({ source: `["foo"]`, target: "valueof string[]", }); From 9804e8dc75b16a9e89fa8aeb13a873538c0637ce Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:36:07 -0700 Subject: [PATCH 082/184] Fix json schema --- .../json-schema/generated-defs/TypeSpec.JsonSchema.ts | 11 +++++++++-- packages/json-schema/src/index.ts | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts index fb52ac4dea..b65738bc4a 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts @@ -1,4 +1,11 @@ -import type { DecoratorContext, ModelProperty, Namespace, Scalar, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + ModelProperty, + Namespace, + Numeric, + Scalar, + Type, +} from "@typespec/compiler"; /** * Add to namespaces to emit models within that namespace to JSON schema. @@ -44,7 +51,7 @@ export type IdDecorator = (context: DecoratorContext, target: Type, id: string) export type MultipleOfDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => void; /** diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 67c0a0cdd7..33ccc7d8a8 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -5,6 +5,7 @@ import { Model, ModelProperty, Namespace, + Numeric, Program, Scalar, Tuple, @@ -123,14 +124,17 @@ const multipleOfKey = createStateSymbol("JsonSchema.multipleOf"); export const $multipleOf: MultipleOfDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, - value: number + value: Numeric ) => { context.program.stateMap(multipleOfKey).set(target, value); }; -export function getMultipleOf(program: Program, target: Type) { +export function getMultipleOfAsNumeric(program: Program, target: Type): Numeric | undefined { return program.stateMap(multipleOfKey).get(target); } +export function getMultipleOf(program: Program, target: Type): number | undefined { + return getMultipleOfAsNumeric(program, target)?.asNumber() ?? undefined; +} const idKey = createStateSymbol("JsonSchema.id"); export const $id: IdDecorator = (context: DecoratorContext, target: Type, value: string) => { From a583297fd1483220bbb234e96c4d494aaa07fd77 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:39:05 -0700 Subject: [PATCH 083/184] more fixes --- packages/json-schema/src/json-schema-emitter.ts | 3 +-- packages/rest/src/resource.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index fa86e51115..eaec06d930 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -37,7 +37,6 @@ import { typespecTypeToJson, Union, UnionVariant, - Value, } from "@typespec/compiler"; import { ArrayBuilder, @@ -182,7 +181,7 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche return result; } - #getDefaultValue(type: Type, defaultType: Type | Value): any { + #getDefaultValue(type: Type, defaultType: Type): any { const program = this.emitter.getProgram(); switch (defaultType.kind) { diff --git a/packages/rest/src/resource.ts b/packages/rest/src/resource.ts index 8c2cd356b7..450fcc800b 100644 --- a/packages/rest/src/resource.ts +++ b/packages/rest/src/resource.ts @@ -156,7 +156,7 @@ export function $copyResourceKeyParameters( return reportNoKeyError(); } - if (templateArguments[0].kind !== "Model") { + if ((templateArguments[0] as any).kind !== "Model") { if (isErrorType(templateArguments[0])) { return; } From 68e87cae64d851cedcca64025de4a27e4f5cb907 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:43:09 -0700 Subject: [PATCH 084/184] Fix up build --- packages/openapi3/src/openapi.ts | 6 +-- packages/openapi3/src/schema-emitter.ts | 53 +++++++----------------- packages/protobuf/src/transform/index.ts | 1 + 3 files changed, 19 insertions(+), 41 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 7b0bff8935..9e489e7bf2 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -390,7 +390,7 @@ function createOAPIEmitter( } const variable: OpenAPI3ServerVariable = { - default: prop.default ? getDefaultValue(program, prop.type, prop.default) : "", + default: prop.defaultValue ? getDefaultValue(program, prop.defaultValue) : "", description: getDoc(program, prop), }; @@ -1276,8 +1276,8 @@ function createOAPIEmitter( return undefined; } const schema = applyEncoding(param, applyIntrinsicDecorators(param, typeSchema)); - if (param.default) { - schema.default = getDefaultValue(program, param.type, param.default); + if (param.defaultValue) { + schema.default = getDefaultValue(program, param.defaultValue); } // Description is already provided in the parameter itself. delete schema.description; diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 0be26eadc5..202946b610 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -358,8 +358,8 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< // Apply decorators on the property to the type's schema const additionalProps: Partial = this.#applyConstraints(prop, {}); - if (prop.default) { - additionalProps.default = getDefaultValue(program, prop.type, prop.default); + if (prop.defaultValue) { + additionalProps.default = getDefaultValue(program, prop.defaultValue); } if (isReadonlyProperty(program, prop)) { @@ -980,47 +980,24 @@ const B = { }, } as const; -export function getDefaultValue(program: Program, type: Type, defaultType: Type | Value): any { - switch (defaultType.kind) { - case "String": - return defaultType.value; - case "Number": +export function getDefaultValue(program: Program, defaultType: Value): any { + switch (defaultType.valueKind) { + case "StringValue": return defaultType.value; - case "Boolean": + case "NumericValue": + return defaultType.value.asNumber() ?? undefined; + case "BooleanValue": return defaultType.value; - case "Tuple": - case "TupleLiteral": - compilerAssert( - type.kind === "Tuple" || (type.kind === "Model" && isArrayModelType(program, type)), - "setting tuple default to non-tuple value" - ); - - if (type.kind === "Tuple") { - return defaultType.values.map((defaultTupleValue, index) => - getDefaultValue(program, type.values[index], defaultTupleValue) - ); - } else { - return defaultType.values.map((defaultTuplevalue) => - getDefaultValue(program, type.indexer!.value, defaultTuplevalue) - ); - } - - case "Intrinsic": - return isNullType(defaultType) - ? null - : reportDiagnostic(program, { - code: "invalid-default", - format: { type: defaultType.kind }, - target: defaultType, - }); - case "EnumMember": - return defaultType.value ?? defaultType.name; - case "UnionVariant": - return getDefaultValue(program, type, defaultType.type); + case "ArrayValue": + return defaultType.values.map((x) => getDefaultValue(program, x)); + case "NullValue": + return null; + case "EnumValue": + return defaultType.value.value ?? defaultType.value.name; default: reportDiagnostic(program, { code: "invalid-default", - format: { type: defaultType.kind }, + format: { type: defaultType.valueKind }, target: defaultType, }); } diff --git a/packages/protobuf/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index 0b42e70744..420c4e74ee 100644 --- a/packages/protobuf/src/transform/index.ts +++ b/packages/protobuf/src/transform/index.ts @@ -1041,6 +1041,7 @@ function getPropertyNameSyntaxTarget(property: ModelProperty): DiagnosticTarget switch (node.kind) { case SyntaxKind.ModelProperty: + case SyntaxKind.ObjectLiteralProperty: return node.id; case SyntaxKind.ModelSpreadProperty: return node; From 6feee8edbe814bf94de57144e85b709f183c61c7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:46:37 -0700 Subject: [PATCH 085/184] regen some docs --- docs/libraries/http/reference/decorators.md | 2 +- packages/http/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/libraries/http/reference/decorators.md b/docs/libraries/http/reference/decorators.md index f9025ae3ba..b6481bdb9d 100644 --- a/docs/libraries/http/reference/decorators.md +++ b/docs/libraries/http/reference/decorators.md @@ -292,7 +292,7 @@ it will be used as a prefix to the route URI of the operation. `@route` can only be applied to operations, namespaces, and interfaces. ```typespec -@TypeSpec.Http.route(path: valueof string, options?: (anonymous model)) +@TypeSpec.Http.route(path: valueof string, options?: { shared: boolean }) ``` #### Target diff --git a/packages/http/README.md b/packages/http/README.md index 4c7ffb90f3..9e26c8cadb 100644 --- a/packages/http/README.md +++ b/packages/http/README.md @@ -337,7 +337,7 @@ it will be used as a prefix to the route URI of the operation. `@route` can only be applied to operations, namespaces, and interfaces. ```typespec -@TypeSpec.Http.route(path: valueof string, options?: (anonymous model)) +@TypeSpec.Http.route(path: valueof string, options?: { shared: boolean }) ``` ##### Target From ccdfeb63810fd6bbb62904712ec67abc8c24fd08 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 13:51:32 -0700 Subject: [PATCH 086/184] some fixes --- packages/http/test/http-decorators.test.ts | 2 +- .../decorators-signatures.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 81cc1c916b..0e0be090f2 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -304,7 +304,7 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: `Argument '(anonymous model)' is not assignable to parameter of type '(anonymous model)'`, + message: `Argument '{ shared: "yes" }' is not assignable to parameter of type '{ shared: boolean }'`, }, ]); }); diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index 0e6eeb130a..42d2f412b8 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -148,8 +148,8 @@ export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${ ["valueof boolean", "boolean"], ["valueof int32", "number"], ["valueof int8", "number"], - ["valueof uint64", "number"], - ["valueof int64", "number"], + ["valueof uint64", "Numeric"], + ["valueof int64", "Numeric"], [`valueof "abc"`, `"abc"`], [`valueof 123`, `123`], [`valueof true`, `true`], @@ -159,7 +159,7 @@ export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${ await expectSignatures({ code: `extern dec simple(target, arg1: ${ref});`, expected: ` -${importLine(["Type"])} +${importLine(["Type", ...(expected === "Numeric" ? ["Numeric"] : [])])} export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => void; `, @@ -207,8 +207,8 @@ export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: ["valueof boolean[]", "boolean[]"], ["valueof int32[]", "number[]"], ["valueof int8[]", "number[]"], - ["valueof uint64[]", "number[]"], - ["valueof int64[]", "number[]"], + ["valueof uint64[]", "Numeric[]"], + ["valueof int64[]", "Numeric[]"], [`valueof "abc"[]`, `"abc"[]`], [`valueof 123[]`, `123[]`], [`valueof true[]`, `true[]`], @@ -218,7 +218,7 @@ export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: await expectSignatures({ code: `extern dec simple(target, ...args: ${ref});`, expected: ` -${importLine(["Type"])} +${importLine(["Type", ...(expected === "Numeric[]" ? ["Numeric"] : [])])} export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: ${expected}) => void; `, From b814cbdf3040de4b2df423aa3a2a7dc59f0e7099 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 14:57:21 -0700 Subject: [PATCH 087/184] Support union variants --- packages/compiler/src/core/checker.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 1baf0c0c70..48d4614020 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -704,6 +704,7 @@ export function createChecker(program: Program): Checker { return null; } + /** In certain context for types that can also be value if the constraint allows it we try to use it as a value instead of a type. */ function tryUsingValueOfType( type: Type, constraint: CheckValueConstraint | undefined, @@ -719,6 +720,8 @@ export function createChecker(program: Program): Checker { return checkBooleanValue(type, constraint, node); case "EnumMember": return checkEnumValue(type, constraint, node); + case "UnionVariant": + return tryUsingValueOfType(type.type, constraint, node); case "Intrinsic": switch (type.name) { case "null": From 7c04ff68771deb916612e33c9a6e656e7e294a43 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 15:01:48 -0700 Subject: [PATCH 088/184] fix protobuf tests --- .../protobuf/test/scenarios/options-invalid/diagnostics.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt b/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt index 904a5d524d..9d8531474f 100644 --- a/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt +++ b/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt @@ -1 +1 @@ -/test/main.tsp:5:10 - error invalid-argument: Argument '(anonymous model)' is not assignable to parameter of type 'TypeSpec.Protobuf.PackageDetails' +/test/main.tsp:5:10 - error invalid-argument: Argument '{ name: "com.azure.Test", options: { java_package: {} } }' is not assignable to parameter of type 'TypeSpec.Protobuf.PackageDetails' From 30590317aa270da5d31fac34f613765a97c58a03 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 15:09:26 -0700 Subject: [PATCH 089/184] http and rest test fix --- packages/http/test/http-decorators.test.ts | 10 +++++----- packages/rest/test/routes.test.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 0e0be090f2..edc62e4c6b 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -85,7 +85,7 @@ describe("http: decorators", () => { { code: "invalid-argument", message: - "Argument '{ name: 123 } is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", + "Argument '{ name: 123 }' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", }, { code: "invalid-argument", @@ -172,7 +172,7 @@ describe("http: decorators", () => { }, { code: "invalid-argument", - message: `Argument '{name: 123}' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'`, + message: `Argument '{ name: 123 }' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'`, }, { code: "invalid-argument", @@ -359,7 +359,7 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", + message: "Argument '123' is not assignable to parameter of type 'string'", }, ]); }); @@ -551,7 +551,7 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", + message: "Argument '123' is not assignable to parameter of type 'string'", }); }); @@ -563,7 +563,7 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", + message: "Argument '123' is not assignable to parameter of type 'string'", }); }); diff --git a/packages/rest/test/routes.test.ts b/packages/rest/test/routes.test.ts index b070d47419..7094b4b448 100644 --- a/packages/rest/test/routes.test.ts +++ b/packages/rest/test/routes.test.ts @@ -425,7 +425,7 @@ describe("rest: routes", () => { strictEqual(diagnostics[0].code, "invalid-argument"); strictEqual( diagnostics[0].message, - `Argument '"x"' is not assignable to parameter of type 'valueof "/" | ":" | "/:"'` + `Argument '"x"' is not assignable to parameter of type '"/" | ":" | "/:"'` ); }); From 560330cac204bdce4a76c528f4a5d41bc3255ec7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 16:00:40 -0700 Subject: [PATCH 090/184] actually cast tuple as value --- packages/compiler/src/core/checker.ts | 72 ++++++++++++++----- .../compiler/test/checker/relation.test.ts | 7 -- .../test/checker/values/array-values.test.ts | 32 ++++++++- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 48d4614020..bf5cdf657c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -722,6 +722,8 @@ export function createChecker(program: Program): Checker { return checkEnumValue(type, constraint, node); case "UnionVariant": return tryUsingValueOfType(type.type, constraint, node); + case "Tuple": + return legacy_tryUsingTupleAsArrayValue(type, constraint?.type, node); case "Intrinsic": switch (type.name) { case "null": @@ -733,6 +735,57 @@ export function createChecker(program: Program): Checker { } } + // Legacy behavior to smooth transition to array values. + function legacy_tryUsingTupleAsArrayValue( + tuple: Tuple, + type: Type | undefined, + node: Node + ): ArrayValue | null { + if ( + tuple.node.kind === SyntaxKind.TupleExpression && + (type === undefined || checkTypeAssignable(tuple, type, node)) + ) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createTupleToLiteralCodeFix(tuple.node)], + format: { + message: + "Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", + }, + target: tuple.node, + }) + ); + } + if (type?.kind === "Model" && isArrayModelType(program, type)) { + return { + valueKind: "ArrayValue", + type, + node: tuple.node as any, + values: tuple.values + .map((x) => + tryUsingValueOfType(x, { kind: "assignment", type: type.indexer.value }, node) + ) + .filter((x): x is Value => x !== null), + }; + } else { + return { + valueKind: "ArrayValue", + type: type ?? tuple, + node: tuple.node as any, + values: tuple.values + .map((x, i) => + tryUsingValueOfType( + x, + type?.kind === "Tuple" ? { kind: "assignment", type: type.values[i] } : undefined, + node + ) + ) + .filter((x): x is Value => x !== null), + }; + } + } + interface CheckConstraint { kind: "argument" | "assignment"; constraint: Type | ValueType | ParamConstraintUnion; @@ -6593,25 +6646,6 @@ export function createChecker(program: Program): Checker { // LEGACY BEHAVIOR - Goal here is to all models instead of object literal and tuple instead of tuple literals to get a smooth migration of decorators if ( - isSourceAType && - source.kind === "Tuple" && - source.node.kind === SyntaxKind.TupleExpression && - isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === - Related.true - ) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "deprecated", - codefixes: [createTupleToLiteralCodeFix(source.node)], - format: { - message: - "Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", - }, - target: source.node, - }) - ); - return [Related.true, []]; - } else if ( isSourceAType && source.kind === "Model" && source.node?.kind === SyntaxKind.ModelExpression && diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 3215564ad8..25d2101da0 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1384,13 +1384,6 @@ describe("compiler: checker: type relations", () => { }); }); - it("can assign a tuple (LEGACY)", async () => { - await expectTypeAssignable({ - source: `["foo"]`, - target: "valueof string[]", - }); - }); - // Disabled for now as this is allowed for backcompat it.skip("cannot assign a tuple", async () => { await expectValueNotAssignable( diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index fa6c6814ca..c89617fbb7 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -1,5 +1,5 @@ import { strictEqual } from "assert"; -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { expectDiagnostics } from "../../../src/testing/index.js"; import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; @@ -82,3 +82,33 @@ describe("emit diagnostic when used in", () => { }); }); }); + +describe("(LEGACY) case tuple to array value", () => { + it("cast the value", async () => { + const value = await compileValueType( + "a", + ` + #suppress "deprecated" "for testing" + const a: string[] = ["foo", "bar"];` + ); + + strictEqual(value.valueKind, "ArrayValue"); + expect(value.values).toHaveLength(2); + strictEqual(value.values[0].valueKind, "StringValue"); + strictEqual(value.values[0].value, "foo"); + strictEqual(value.values[1].valueKind, "StringValue"); + strictEqual(value.values[1].value, "bar"); + }); + it("emit a warning diagnostic", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + const a: string[] = ┆["foo"]; + `); + + expectDiagnostics(diagnostics, { + code: "deprecated", + message: + "Deprecated: Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", + pos, + }); + }); +}); From f53a83a312635c8b612052afd8af286749d4a2f2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 20:12:57 -0700 Subject: [PATCH 091/184] Better conversion of legacy model and tuple to values --- packages/compiler/src/core/checker.ts | 210 ++++++++++-------- packages/compiler/src/core/js-marshaller.ts | 2 +- packages/compiler/src/core/numeric-ranges.ts | 33 +++ .../compiler/test/checker/decorators.test.ts | 3 +- .../compiler/test/checker/relation.test.ts | 8 - .../test/checker/values/array-values.test.ts | 58 ++++- .../test/checker/values/object-values.test.ts | 65 +++++- packages/openapi/test/decorators.test.ts | 4 - 8 files changed, 261 insertions(+), 122 deletions(-) create mode 100644 packages/compiler/src/core/numeric-ranges.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index bf5cdf657c..4b01d64221 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -16,6 +16,7 @@ import { } from "./helpers/index.js"; import { marshallTypeForJSWithLegacyCast } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; +import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; import { exprIsBareIdentifier, @@ -708,7 +709,8 @@ export function createChecker(program: Program): Checker { function tryUsingValueOfType( type: Type, constraint: CheckValueConstraint | undefined, - node: Node + node: Node, + options: { legacyTupleAndModelCast?: boolean } = {} ): Type | Value | null { switch (type.kind) { case "String": @@ -723,7 +725,13 @@ export function createChecker(program: Program): Checker { case "UnionVariant": return tryUsingValueOfType(type.type, constraint, node); case "Tuple": - return legacy_tryUsingTupleAsArrayValue(type, constraint?.type, node); + return options.legacyTupleAndModelCast + ? legacy_tryUsingTupleAsArrayValue(type, constraint?.type, node) + : type; + case "Model": + return options.legacyTupleAndModelCast + ? legacy_tryUsingModelAsObjectValue(type, constraint?.type, node) + : type; case "Intrinsic": switch (type.name) { case "null": @@ -735,55 +743,116 @@ export function createChecker(program: Program): Checker { } } + // Legacy behavior to smooth transition to object values. + function legacy_tryUsingModelAsObjectValue( + model: Model, + type: Type | undefined, + node: Node + ): Model | ObjectValue | null { + if (model.node?.kind !== SyntaxKind.ModelExpression) { + return model; // we only want to convert model expressions + } + + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createModelToLiteralCodeFix(model.node)], + format: { + message: + "Using a model as a value is deprecated. Use an object literal instead(with #{}).", + }, + target: model.node, + }) + ); + + const value: ObjectValue = { + valueKind: "ObjectValue", + type: type ?? model, + node: model.node as any, + properties: new Map(), + }; + + for (const prop of model.properties.values()) { + const propValue = tryUsingValueOfType( + prop.type, + { kind: "assignment", type: prop.type }, + node, + { legacyTupleAndModelCast: true } + ); + if (propValue == null) { + return null; + } else if (!isValue(propValue)) { + return model; + } + value.properties.set(prop.name, { + name: prop.name, + value: propValue, + node: prop.node as any, + }); + } + + if (type !== undefined && !checkTypeAssignable(model, type, node)) { + return null; + } + + return value; + } + // Legacy behavior to smooth transition to array values. function legacy_tryUsingTupleAsArrayValue( tuple: Tuple, type: Type | undefined, node: Node - ): ArrayValue | null { - if ( - tuple.node.kind === SyntaxKind.TupleExpression && - (type === undefined || checkTypeAssignable(tuple, type, node)) - ) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "deprecated", - codefixes: [createTupleToLiteralCodeFix(tuple.node)], - format: { - message: - "Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", - }, - target: tuple.node, - }) + ): Tuple | ArrayValue | null { + if (tuple.node.kind !== SyntaxKind.TupleExpression) { + return tuple; // we won't convert dynamic tuples to array values + } + + reportCheckerDiagnostic( + createDiagnostic({ + code: "deprecated", + codefixes: [createTupleToLiteralCodeFix(tuple.node)], + format: { + message: "Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", + }, + target: tuple.node, + }) + ); + + const values: Value[] = []; + for (const [index, item] of tuple.values.entries()) { + const itemType = + type?.kind === "Model" && isArrayModelType(program, type) + ? type.indexer.value + : type?.kind === "Tuple" + ? type.values[index] + : undefined; + const value = tryUsingValueOfType( + item, + itemType && { kind: "assignment", type: itemType }, + node, + { + legacyTupleAndModelCast: true, + } ); + if (value === null) { + return null; + } else if (!isValue(value)) { + return tuple; + } + values.push(value); } - if (type?.kind === "Model" && isArrayModelType(program, type)) { - return { - valueKind: "ArrayValue", - type, - node: tuple.node as any, - values: tuple.values - .map((x) => - tryUsingValueOfType(x, { kind: "assignment", type: type.indexer.value }, node) - ) - .filter((x): x is Value => x !== null), - }; - } else { - return { - valueKind: "ArrayValue", - type: type ?? tuple, - node: tuple.node as any, - values: tuple.values - .map((x, i) => - tryUsingValueOfType( - x, - type?.kind === "Tuple" ? { kind: "assignment", type: type.values[i] } : undefined, - node - ) - ) - .filter((x): x is Value => x !== null), - }; + + if (type !== undefined && !checkTypeAssignable(tuple, type, node)) { + return null; } + + return { + valueKind: "ArrayValue", + type: type ?? tuple, + node: tuple.node as any, + values, + }; } interface CheckConstraint { @@ -811,7 +880,7 @@ export function createChecker(program: Program): Checker { } if (valueConstraint) { - return tryUsingValueOfType(entity, valueConstraint, node); + return tryUsingValueOfType(entity, valueConstraint, node, { legacyTupleAndModelCast: true }); } return entity; @@ -1356,7 +1425,7 @@ export function createChecker(program: Program): Checker { ? finalMap.get(param.constraint)! : param.constraint; - if (!checkTypeAssignable(type, constraint, argNode)) { + if (isErrorType(type) || !checkTypeAssignable(type, constraint, argNode)) { // TODO-TIM check if we expose this below const effectiveType = param.constraint?.kind === "Value" || param.constraint.kind === "ParamConstraintUnion" @@ -6644,28 +6713,6 @@ export function createChecker(program: Program): Checker { ); } - // LEGACY BEHAVIOR - Goal here is to all models instead of object literal and tuple instead of tuple literals to get a smooth migration of decorators - if ( - isSourceAType && - source.kind === "Model" && - source.node?.kind === SyntaxKind.ModelExpression && - isTypeAssignableToInternal(source, target.target, diagnosticTarget, relationCache)[0] === - Related.true - ) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "deprecated", - codefixes: [createModelToLiteralCodeFix(source.node)], - format: { - message: - "Using a model as a value is deprecated. Use an object literal instead(with #{}).", - }, - target: source, - }) - ); - return [Related.true, []]; - } - if (!isValue(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } @@ -7079,35 +7126,6 @@ function isAnonymous(type: Type) { return !("name" in type) || typeof type.name !== "string" || !type.name; } -export const numericRanges = { - int64: [ - Numeric("-9223372036854775808"), - Numeric("9223372036854775807"), - { int: true, isJsNumber: false }, - ], - int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true, isJsNumber: true }], - int16: [Numeric("-32768"), Numeric("32767"), { int: true, isJsNumber: true }], - int8: [Numeric("-128"), Numeric("127"), { int: true, isJsNumber: true }], - uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true, isJsNumber: false }], - uint32: [Numeric("0"), Numeric("4294967295"), { int: true, isJsNumber: true }], - uint16: [Numeric("0"), Numeric("65535"), { int: true, isJsNumber: true }], - uint8: [Numeric("0"), Numeric("255"), { int: true, isJsNumber: true }], - safeint: [ - Numeric(Number.MIN_SAFE_INTEGER.toString()), - Numeric(Number.MAX_SAFE_INTEGER.toString()), - { int: true, isJsNumber: true }, - ], - float32: [Numeric("-3.4e38"), Numeric("3.4e38"), { int: false, isJsNumber: true }], - float64: [ - Numeric(`${-Number.MAX_VALUE}`), - Numeric(Number.MAX_VALUE.toString()), - { int: false, isJsNumber: true }, - ], -} as const satisfies Record< - string, - [min: Numeric, max: Numeric, options: { int: boolean; isJsNumber: boolean }] ->; - /** * Find all named models that could have been the source of the given * property. This includes the named parents of all property sources in a diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index a72ebc4519..6c3b0e7677 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,6 +1,6 @@ -import { numericRanges } from "./checker.js"; import { typespecTypeToJson } from "./decorator-utils.js"; import { compilerAssert } from "./diagnostics.js"; +import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; import type { ArrayValue, diff --git a/packages/compiler/src/core/numeric-ranges.ts b/packages/compiler/src/core/numeric-ranges.ts new file mode 100644 index 0000000000..18d7eb3a7c --- /dev/null +++ b/packages/compiler/src/core/numeric-ranges.ts @@ -0,0 +1,33 @@ +import { Numeric } from "./numeric.js"; + +/** + * Set of known numeric ranges + */ +export const numericRanges = { + int64: [ + Numeric("-9223372036854775808"), + Numeric("9223372036854775807"), + { int: true, isJsNumber: false }, + ], + int32: [Numeric("-2147483648"), Numeric("2147483647"), { int: true, isJsNumber: true }], + int16: [Numeric("-32768"), Numeric("32767"), { int: true, isJsNumber: true }], + int8: [Numeric("-128"), Numeric("127"), { int: true, isJsNumber: true }], + uint64: [Numeric("0"), Numeric("18446744073709551615"), { int: true, isJsNumber: false }], + uint32: [Numeric("0"), Numeric("4294967295"), { int: true, isJsNumber: true }], + uint16: [Numeric("0"), Numeric("65535"), { int: true, isJsNumber: true }], + uint8: [Numeric("0"), Numeric("255"), { int: true, isJsNumber: true }], + safeint: [ + Numeric(Number.MIN_SAFE_INTEGER.toString()), + Numeric(Number.MAX_SAFE_INTEGER.toString()), + { int: true, isJsNumber: true }, + ], + float32: [Numeric("-3.4e38"), Numeric("3.4e38"), { int: false, isJsNumber: true }], + float64: [ + Numeric(`${-Number.MAX_VALUE}`), + Numeric(Number.MAX_VALUE.toString()), + { int: false, isJsNumber: true }, + ], +} as const satisfies Record< + string, + [min: Numeric, max: Numeric, options: { int: boolean; isJsNumber: boolean }] +>; diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 10f05fd9c8..d3b72c30b0 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,6 +1,7 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { numericRanges, setTypeSpecNamespace } from "../../src/core/index.js"; +import { setTypeSpecNamespace } from "../../src/core/index.js"; +import { numericRanges } from "../../src/core/numeric-ranges.js"; import { Numeric } from "../../src/core/numeric.js"; import { BasicTestRunner, diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 25d2101da0..333d7d2d01 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1293,14 +1293,6 @@ describe("compiler: checker: type relations", () => { }); }); - it("can assign a model (LEGACY)", async () => { - await expectTypeAssignable({ - source: `{name: "foo"}`, - target: "valueof Info", - commonCode: `model Info { name: string }`, - }); - }); - // Disabled for now as this is allowed for backcompat it.skip("cannot assign a model ", async () => { await expectTypeNotAssignable( diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index c89617fbb7..35b7510e92 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -1,6 +1,7 @@ -import { strictEqual } from "assert"; +import { ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; -import { expectDiagnostics } from "../../../src/testing/index.js"; +import { Model, isValue } from "../../../src/index.js"; +import { createTestRunner, expectDiagnostics } from "../../../src/testing/index.js"; import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; it("no values", async () => { @@ -83,15 +84,20 @@ describe("emit diagnostic when used in", () => { }); }); -describe("(LEGACY) case tuple to array value", () => { - it("cast the value", async () => { - const value = await compileValueType( - "a", +describe("(LEGACY) cast tuple to array value", () => { + it("create the value", async () => { + const runner = await createTestRunner(); + const { Test } = (await runner.compile( ` + @test model Test {} + #suppress "deprecated" "for testing" - const a: string[] = ["foo", "bar"];` - ); + alias A = Test<["foo", "bar"]>; + ` + )) as { Test: Model }; + const value = Test.templateMapper?.args[0]; + ok(value && isValue(value)); strictEqual(value.valueKind, "ArrayValue"); expect(value.values).toHaveLength(2); strictEqual(value.values[0].valueKind, "StringValue"); @@ -99,9 +105,11 @@ describe("(LEGACY) case tuple to array value", () => { strictEqual(value.values[1].valueKind, "StringValue"); strictEqual(value.values[1].value, "bar"); }); + it("emit a warning diagnostic", async () => { const { diagnostics, pos } = await diagnoseUsage(` - const a: string[] = ┆["foo"]; + model Test {} + alias A = Test<┆["foo"]>; `); expectDiagnostics(diagnostics, { @@ -111,4 +119,36 @@ describe("(LEGACY) case tuple to array value", () => { pos, }); }); + + it("emit a error if element in tuple expression are not castable to value", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + + #suppress "deprecated" "for testing" + alias A = Test<┆[string]>; + `); + + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '[string]' is not assignable to type 'valueof string[]'", + pos, + }); + }); + + it("emit a error if element in tuple expression are not assignable", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + + alias A = Test<┆[123]>; + `); + + expectDiagnostics(diagnostics, [ + { code: "deprecated" }, // deprecated diagnostic still emitted + { + code: "unassignable", + message: "Type '123' is not assignable to type 'string'", + pos, + }, + ]); + }); }); diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index 83f64c11c3..d3071a52a4 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -1,6 +1,7 @@ -import { strictEqual } from "assert"; -import { describe, it } from "vitest"; -import { expectDiagnostics } from "../../../src/testing/index.js"; +import { ok, strictEqual } from "assert"; +import { describe, expect, it } from "vitest"; +import { Model, isValue } from "../../../src/index.js"; +import { createTestRunner, expectDiagnostics } from "../../../src/testing/index.js"; import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; it("no properties", async () => { @@ -147,3 +148,61 @@ describe("emit diagnostic when used in", () => { }); }); }); + +describe("(LEGACY) cast model to object value", () => { + it("create the value", async () => { + const runner = await createTestRunner(); + const { Test } = (await runner.compile( + ` + @test model Test {} + + #suppress "deprecated" "for testing" + alias A = Test<{a: "foo", b: "bar"}>; + ` + )) as { Test: Model }; + + const value = Test.templateMapper?.args[0]; + ok(value && isValue(value)); + strictEqual(value.valueKind, "ObjectValue"); + expect(value.properties).toHaveLength(2); + const a = value.properties.get("a")?.value; + ok(a); + strictEqual(a.valueKind, "StringValue"); + strictEqual(a.value, "foo"); + const b = value.properties.get("b")?.value; + ok(b); + strictEqual(b.valueKind, "StringValue"); + strictEqual(b.value, "bar"); + }); + + it("emit a warning diagnostic", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + alias A = Test<┆{a: "b"}>; + `); + + expectDiagnostics(diagnostics, { + code: "deprecated", + message: + "Deprecated: Using a model as a value is deprecated. Use an object literal instead(with #{}).", + pos, + }); + }); + + it("emit a error if element in model expression are not castable to value", async () => { + const { diagnostics, pos } = await diagnoseUsage(` + model Test {} + + alias A = Test<┆{a: string}>; + `); + + expectDiagnostics(diagnostics, [ + { code: "deprecated" }, // deprecated diagnostic still emitted + { + code: "unassignable", + message: "Type '{ a: string }' is not assignable to type 'valueof { a: string }'", + pos, + }, + ]); + }); +}); diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index fb6ae8a92e..30c349d87e 100644 --- a/packages/openapi/test/decorators.test.ts +++ b/packages/openapi/test/decorators.test.ts @@ -34,7 +34,6 @@ describe("openapi: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); }); @@ -79,7 +78,6 @@ describe("openapi: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); @@ -108,7 +106,6 @@ describe("openapi: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); @@ -121,7 +118,6 @@ describe("openapi: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); From 82601cb2a4dfb66c73ca88d71cff8c81587db1cc Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 20:19:23 -0700 Subject: [PATCH 092/184] default can also cast tuple --- packages/compiler/src/core/checker.ts | 22 +++++++++++++++------- packages/openapi3/test/decorators.test.ts | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 4b01d64221..64dab7eb67 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -685,13 +685,14 @@ export function createChecker(program: Program): Checker { function getValueForNode( node: Node, mapper?: TypeMapper, - constraint?: CheckValueConstraint + constraint?: CheckValueConstraint, + options: { legacyTupleAndModelCast?: boolean } = {} ): Value | null { let entity = getTypeOrValueForNodeInternal(node, mapper, constraint); if (entity === null || isValue(entity)) { return entity; } - entity = tryUsingValueOfType(entity, constraint, node); + entity = tryUsingValueOfType(entity, constraint, node, options); if (entity === null || isValue(entity)) { return entity; } @@ -4446,10 +4447,15 @@ export function createChecker(program: Program): Checker { // if the prop type is an error we don't need to validate again. return null; } - const defaultValue = getValueForNode(defaultNode, undefined, { - kind: "assignment", - type, - }); + const defaultValue = getValueForNode( + defaultNode, + undefined, + { + kind: "assignment", + type, + }, + { legacyTupleAndModelCast: true } + ); if (defaultValue === null) { return null; } @@ -4461,7 +4467,9 @@ export function createChecker(program: Program): Checker { return defaultValue; } } - /** Fill in the legacy `.default` property. + + /** + * Fill in the legacy `.default` property. * We do do checking here we just keep existing behavior. */ function checkLegacyDefault(defaultNode: Node): Type | undefined { diff --git a/packages/openapi3/test/decorators.test.ts b/packages/openapi3/test/decorators.test.ts index bf4810c0eb..98f1f31ff6 100644 --- a/packages/openapi3/test/decorators.test.ts +++ b/packages/openapi3/test/decorators.test.ts @@ -36,7 +36,6 @@ describe("openapi3: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); From 56deca2eb7e98c05032d15618f259e9535a3a5ea Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 20:42:29 -0700 Subject: [PATCH 093/184] format --- packages/http/lib/auth.tsp | 2 +- .../specs/rest-metadata-emitter/rest-metadata-emitter-sample.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http/lib/auth.tsp b/packages/http/lib/auth.tsp index 1cf81dd2a3..284af5f90f 100644 --- a/packages/http/lib/auth.tsp +++ b/packages/http/lib/auth.tsp @@ -212,7 +212,7 @@ model ClientCredentialsFlow { * https://server.com/.well-known/openid-configuration * ``` */ -model OpenIdConnectAuth { +model OpenIdConnectAuth { /** Auth type */ type: AuthType.openIdConnect; diff --git a/packages/samples/specs/rest-metadata-emitter/rest-metadata-emitter-sample.ts b/packages/samples/specs/rest-metadata-emitter/rest-metadata-emitter-sample.ts index 53ed09eaf6..78a4d404a0 100644 --- a/packages/samples/specs/rest-metadata-emitter/rest-metadata-emitter-sample.ts +++ b/packages/samples/specs/rest-metadata-emitter/rest-metadata-emitter-sample.ts @@ -51,6 +51,7 @@ export async function $onEmit(context: EmitContext): Promise { projectedProgram, serviceNamespace, details?.title, + // eslint-disable-next-line deprecation/deprecation versionProjection.version ?? details?.version ); } @@ -135,6 +136,7 @@ export async function $onEmit(context: EmitContext): Promise { function emitResponses(responses: HttpOperationResponse[]) { for (const response of responses) { for (const content of response.responses) { + // eslint-disable-next-line deprecation/deprecation writeLine(`response: ${response.statusCode}${getContentTypeRemark(content.body)}`); indent(); From b6f4f615b7e25d83b7d0d53b34d225fc08f5a092 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 12 Apr 2024 21:04:18 -0700 Subject: [PATCH 094/184] merge with main --- packages/compiler/src/server/completion.ts | 2 +- packages/compiler/test/server/completion.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index e886b48068..92d9cbd856 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -56,7 +56,7 @@ export async function resolveCompletion( addKeywordCompletion("namespace", context.completions); break; case SyntaxKind.ScalarStatement: - addKeywordCompletion("scalar", completions); + addKeywordCompletion("scalar", context.completions); break; case SyntaxKind.Identifier: addDirectiveCompletion(context, node); diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index efc36d3ee4..0c21962680 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -33,6 +33,7 @@ describe("complete statement keywords", () => { ["union", true], ["enum", true], ["fn", true], + ["const", true], ])("%s", (keyword, inNamespace) => { describe.each(inNamespace ? ["top level", "namespace"] : ["top level"])("%s", () => { it("complete with no text", async () => { From bbf9022ee5e7a1cf196204ec443fb0ece55336cb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 13 Apr 2024 09:19:42 -0700 Subject: [PATCH 095/184] remove unnecessary marshalling --- packages/compiler/src/core/checker.ts | 2 +- packages/compiler/src/core/js-marshaller.ts | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 64dab7eb67..054e966875 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4710,7 +4710,7 @@ export function createChecker(program: Program): Checker { valueConstraint: CheckValueConstraint | undefined ) { if (valueConstraint !== undefined) { - if (isValue(value) || value.kind === "Model" || value.kind === "Tuple") { + if (isValue(value)) { const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value, valueConstraint.type); reportCheckerDiagnostics(diagnostics); return res ?? value; diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 6c3b0e7677..8fe4a79a58 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,4 +1,3 @@ -import { typespecTypeToJson } from "./decorator-utils.js"; import { compilerAssert } from "./diagnostics.js"; import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; @@ -6,24 +5,18 @@ import type { ArrayValue, Diagnostic, MarshalledValue, - Model, NumericValue, ObjectValue, - Tuple, Type, Value, } from "./types.js"; /** Legacy version that will cast models to object literals and tuple to tuple literals */ -export function marshallTypeForJSWithLegacyCast( - entity: T, +export function marshallTypeForJSWithLegacyCast( + value: Value, valueConstraint: Type ): [MarshalledValue | undefined, readonly Diagnostic[]] { - if ("kind" in entity) { - return typespecTypeToJson(entity, entity) as any; - } else { - return [marshallTypeForJS(entity, valueConstraint) as any, []]; - } + return [marshallTypeForJS(value, valueConstraint) as any, []]; } export function marshallTypeForJS( type: T, From 64c371b038c5482563c865612422e08f23a5d581 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 15 Apr 2024 15:56:04 -0700 Subject: [PATCH 096/184] Better string template handling --- packages/compiler/src/core/checker.ts | 87 +++++++++++++++---- .../src/core/helpers/string-template-utils.ts | 5 ++ packages/compiler/src/core/messages.ts | 7 ++ .../test/checker/values/string-values.test.ts | 7 ++ .../samples/specs/string-template/main.tsp | 4 +- 5 files changed, 91 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 054e966875..c5bda5fd10 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2987,19 +2987,75 @@ export function createChecker(program: Program): Checker { function checkStringTemplateExpresion( node: StringTemplateExpressionNode, mapper: TypeMapper | undefined - ): StringTemplate { - const spans: StringTemplateSpan[] = [createTemplateSpanLiteral(node.head)]; - for (const span of node.spans) { - spans.push(createTemplateSpanValue(span.expression, mapper)); - spans.push(createTemplateSpanLiteral(span.literal)); - } - const type = createType({ - kind: "StringTemplate", - node, - spans, - }); + ): StringTemplate | StringValue | null { + let hasType = false; + let hasValue = false; + const spanTypeOrValues = node.spans.map( + (span) => [span, getTypeOrValueForNode(span.expression, mapper)] as const + ); + for (const [_, typeOrValue] of spanTypeOrValues) { + if (typeOrValue !== null) { + if (isValue(typeOrValue)) { + hasValue = true; + } else { + hasType = true; + } + } + } - return type; + if (hasType && hasValue) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "mixed-string-template", + target: node, + }) + ); + } + + if (hasValue) { + let str = node.head.value; + for (const [span, typeOrValue] of spanTypeOrValues) { + compilerAssert(typeOrValue !== null && isValue(typeOrValue), "Expected value."); + str += stringifyValueForTemplate(typeOrValue); + str += span.literal.value; + } + return checkStringValue(createLiteralType(str), undefined, node); // TODO: constraint + } else { + const spans: StringTemplateSpan[] = [createTemplateSpanLiteral(node.head)]; + + for (const [span, typeOrValue] of spanTypeOrValues) { + if (typeOrValue !== null) { + if (isValue(typeOrValue)) { + hasValue = true; + } else { + hasType = true; + spans.push(createTemplateSpanValue(span.expression, typeOrValue)); + } + } + spans.push(createTemplateSpanLiteral(span.literal)); + } + return createType({ + kind: "StringTemplate", + node, + spans, + }); + } + } + function stringifyValueForTemplate(value: Value): string { + switch (value.valueKind) { + case "StringValue": + case "NumericValue": + case "BooleanValue": + return value.value.toString(); + default: + reportCheckerDiagnostic( + createDiagnostic({ + code: "non-literal-string-template", + target: value, + }) + ); + return `[${value.valueKind}]`; + } } function createTemplateSpanLiteral( @@ -3013,15 +3069,12 @@ export function createChecker(program: Program): Checker { }); } - function createTemplateSpanValue( - node: Expression, - mapper: TypeMapper | undefined - ): StringTemplateSpanValue { + function createTemplateSpanValue(node: Expression, type: Type): StringTemplateSpanValue { return createType({ kind: "StringTemplateSpan", node: node, isInterpolated: true, - type: getTypeForNode(node, mapper), + type: type, }); } diff --git a/packages/compiler/src/core/helpers/string-template-utils.ts b/packages/compiler/src/core/helpers/string-template-utils.ts index e466281a23..1cab20d741 100644 --- a/packages/compiler/src/core/helpers/string-template-utils.ts +++ b/packages/compiler/src/core/helpers/string-template-utils.ts @@ -24,6 +24,11 @@ export function stringTemplateToString( return String(x.type.value); case "StringTemplate": return diagnostics.pipe(stringTemplateToString(x.type)); + case "TemplateParameter": + if (x.type.constraint && x.type.constraint.kind === "Value") { + return ""; + } + // eslint-disable-next-line no-fallthrough default: diagnostics.add( createDiagnostic({ diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index e1103c801f..d5757c262c 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -663,6 +663,13 @@ const diagnostics = { "Projections are experimental - your code will need to change as this feature evolves.", }, }, + "mixed-string-template": { + severity: "error", + messages: { + default: + "String template is interpolating values and types. It must be either all values to produce a string value or or all types for string template type.", + }, + }, "non-literal-string-template": { severity: "error", messages: { diff --git a/packages/compiler/test/checker/values/string-values.test.ts b/packages/compiler/test/checker/values/string-values.test.ts index 5e505bd07c..545368b400 100644 --- a/packages/compiler/test/checker/values/string-values.test.ts +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -63,6 +63,13 @@ describe("string templates", () => { strictEqual(value.scalar?.name, "string"); strictEqual(value.value, "one abc def"); }); + + it("interpolate another const", async () => { + const value = await compileValueType(`string("one \${a} def")`, `const a = "abc";`); + strictEqual(value.valueKind, "StringValue"); + strictEqual(value.value, "one abc def"); + }); + it("emit error if string template is not serializable to string", async () => { const diagnostics = await diagnoseValueType(`string("one \${boolean} def")`); expectDiagnostics(diagnostics, { diff --git a/packages/samples/specs/string-template/main.tsp b/packages/samples/specs/string-template/main.tsp index 330845b4f7..61c13e1327 100644 --- a/packages/samples/specs/string-template/main.tsp +++ b/packages/samples/specs/string-template/main.tsp @@ -12,11 +12,11 @@ model Person { template: Template<"custom">; } -alias Template = "Foo ${T} bar"; +alias Template = "Foo ${T} bar"; /** Example of string template with template parameters */ @doc("Animal named: ${T}") -model Animal { +model Animal { kind: T; } From 008ba344831bfad433d10d0c242a5e2fb3ed9742 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 15 Apr 2024 16:48:14 -0700 Subject: [PATCH 097/184] . --- packages/samples/specs/string-template/main.tsp | 4 ++-- .../output/string-template/@typespec/openapi3/openapi.yaml | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/samples/specs/string-template/main.tsp b/packages/samples/specs/string-template/main.tsp index 61c13e1327..ba422dab7f 100644 --- a/packages/samples/specs/string-template/main.tsp +++ b/packages/samples/specs/string-template/main.tsp @@ -16,8 +16,8 @@ alias Template = "Foo ${T} bar"; /** Example of string template with template parameters */ @doc("Animal named: ${T}") -model Animal { - kind: T; +model Animal { + named: string; } model Cat is Animal<"Cat">; diff --git a/packages/samples/test/output/string-template/@typespec/openapi3/openapi.yaml b/packages/samples/test/output/string-template/@typespec/openapi3/openapi.yaml index 28951f5f7a..613d53fda5 100644 --- a/packages/samples/test/output/string-template/@typespec/openapi3/openapi.yaml +++ b/packages/samples/test/output/string-template/@typespec/openapi3/openapi.yaml @@ -9,12 +9,10 @@ components: Cat: type: object required: - - kind + - named properties: - kind: + named: type: string - enum: - - Cat description: 'Animal named: Cat' Person: type: object From dfe0bf26fa6a8610a7f7e3745e4a16c2718e6068 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 15 Apr 2024 17:19:19 -0700 Subject: [PATCH 098/184] missing return --- packages/compiler/src/core/checker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c5bda5fd10..e31f88b541 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3010,6 +3010,7 @@ export function createChecker(program: Program): Checker { target: node, }) ); + return null; } if (hasValue) { From 188455942457b6d0a9c2825996822d2c1c59a778 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 15 Apr 2024 17:33:35 -0700 Subject: [PATCH 099/184] string value test --- .../test/checker/values/string-values.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/compiler/test/checker/values/string-values.test.ts b/packages/compiler/test/checker/values/string-values.test.ts index 545368b400..9f3b2f94aa 100644 --- a/packages/compiler/test/checker/values/string-values.test.ts +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -78,6 +78,18 @@ describe("string templates", () => { "Value interpolated in this string template cannot be converted to a string. Only literal types can be automatically interpolated.", }); }); + + it("emit error if string template if interpolating non serializable value", async () => { + const diagnostics = await diagnoseValueType( + `string("one \${a} def")`, + `const a = #{a: "foo"};` + ); + expectDiagnostics(diagnostics, { + code: "non-literal-string-template", + message: + "Value interpolated in this string template cannot be converted to a string. Only literal types can be automatically interpolated.", + }); + }); }); describe("validate literal are assignable", () => { From f1ae0817e169ea0b644f0c707f815fde53b1b37e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 15 Apr 2024 20:42:47 -0700 Subject: [PATCH 100/184] Opt-in new marshalling --- packages/compiler/src/core/binder.ts | 8 +- packages/compiler/src/core/checker.ts | 174 ++++++++++++------ packages/compiler/src/core/index.ts | 1 + packages/compiler/src/core/js-marshaller.ts | 54 ++++-- packages/compiler/src/core/library.ts | 5 + packages/compiler/src/core/projector.ts | 2 +- packages/compiler/src/core/types.ts | 51 ++++- .../compiler/test/checker/decorators.test.ts | 159 +++++++++++++++- packages/json-schema/lib/main.tsp | 1 + packages/json-schema/src/lib.ts | 15 +- 10 files changed, 383 insertions(+), 87 deletions(-) diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 7d0b5fd361..eb36ca53d4 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -134,8 +134,12 @@ export function createBinder(program: Program): Binder { let name: string; let kind: "decorator" | "function"; let containerSymbol = sourceFile.symbol; - - if (typeof member === "function") { + if (key === "$flags") { + const context = getLocationContext(program, sourceFile); + if (context.type === "library" || context.type === "project") { + mutate(context).flags = member as any; + } + } else if (typeof member === "function") { // lots of 'any' casts here because control flow narrowing `member` to Function // isn't particularly useful it turns out. if (isFunctionName(key)) { diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e31f88b541..f4fb6227de 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5,16 +5,26 @@ import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-iden import { createModelToLiteralCodeFix } from "./compiler-code-fixes/model-to-literal.codefix.js"; import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; -import { ProjectionError, compilerAssert, reportDeprecated } from "./diagnostics.js"; +import { + ProjectionError, + compilerAssert, + ignoreDiagnostics, + reportDeprecated, +} from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { TypeNameOptions, getEntityName, + getLocationContext, getNamespaceFullName, getTypeName, stringTemplateToString, } from "./helpers/index.js"; -import { marshallTypeForJSWithLegacyCast } from "./js-marshaller.js"; +import { + canNumericConstraintBeJsNumber, + legacyMarshallTypeForJS, + marshallTypeForJS, +} from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; @@ -275,10 +285,11 @@ export interface Checker { /** @internal */ getValueForNode(node: Node): Value | null; - errorType: ErrorType; - voidType: VoidType; - neverType: NeverType; - anyType: UnknownType; + readonly errorType: ErrorType; + readonly voidType: VoidType; + readonly neverType: NeverType; + readonly nullType: NullType; + readonly anyType: UnknownType; } interface TypePrototype { @@ -418,6 +429,7 @@ export function createChecker(program: Program): Checker { project, neverType, errorType, + nullType, anyType: unknownType, voidType, typePrototype, @@ -542,7 +554,7 @@ export function createChecker(program: Program): Checker { if (ref.flags & SymbolFlags.Namespace) { const links = getSymbolLinks(getMergedSymbol(ref)); const type: Type & DecoratedType = links.type! as any; - const decApp = checkDecorator(type, decNode, undefined); + const decApp = checkDecoratorApplication(type, decNode, undefined); if (decApp) { type.decorators.push(decApp); applyDecoratorToType(program, decApp, type); @@ -1851,9 +1863,48 @@ export function createChecker(program: Program): Checker { linkType(links, decoratorType, mapper); + checkDecoratorLegacyMarshalling(decoratorType); return decoratorType; } + function checkDecoratorLegacyMarshalling(decorator: Decorator) { + const marshalling = resolveDecoratorArgMarshalling(decorator); + if (marshalling === "legacy") { + for (const param of decorator.parameters) { + if (param.type.kind === "Value") { + if ( + ignoreDiagnostics(isTypeAssignableTo(nullType, param.type.target, param.type.target)) + ) { + reportDeprecated( + program, + [ + `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting null as a type.`, + `This will change in the future.`, + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "value"}}` to your library.', + ].join("\n"), + param.node + ); + } else if ( + ignoreDiagnostics( + isTypeAssignableTo(param.type.target, getStdType("numeric"), param.type.target) + ) && + !canNumericConstraintBeJsNumber(param.type.target) + ) { + reportDeprecated( + program, + [ + `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting a numeric type that is not representable as a JS Number.`, + `This will change in the future.`, + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "value"}}` to your library.', + ].join("\n"), + param.node + ); + } + } + } + } + } + function checkFunctionDeclaration( node: FunctionDeclarationStatementNode, mapper: TypeMapper | undefined @@ -4534,7 +4585,7 @@ export function createChecker(program: Program): Checker { return resolved; } - function checkDecorator( + function checkDecoratorApplication( targetType: Type, decNode: DecoratorExpressionNode | AugmentDecoratorStatementNode, mapper: TypeMapper | undefined @@ -4591,6 +4642,7 @@ export function createChecker(program: Program): Checker { if (hasError || argsHaveError) { return undefined; } + return { definition: symbolLinks.declaredType, decorator: sym.value ?? ((...args: any[]) => {}), @@ -4599,6 +4651,22 @@ export function createChecker(program: Program): Checker { }; } + function resolveDecoratorArgMarshalling(declaredType: Decorator | undefined): "value" | "legacy" { + if (declaredType) { + const location = getLocationContext(program, declaredType); + if (location.type === "compiler") { + return "value"; + } else if ( + (location.type === "library" || location.type === "project") && + location.flags?.decoratorArgMarshalling + ) { + return location.flags.decoratorArgMarshalling; + } else { + return "legacy"; + } + } + return "value"; + } /** Check the decorator target is valid */ function checkDecoratorTarget(targetType: Type, declaration: Decorator, decoratorNode: Node) { @@ -4679,7 +4747,36 @@ export function createChecker(program: Program): Checker { } const resolvedArgs: DecoratorArgument[] = []; - + const jsMarshalling = resolveDecoratorArgMarshalling(declaration); + function resolveArg( + argNode: Expression, + perParamType: Type | ValueType | ParamConstraintUnion + ): DecoratorArgument | undefined { + const arg = getTypeOrValueForNode(argNode, mapper, { + kind: "argument", + constraint: perParamType, + }); + if ( + arg !== null && + !(isType(arg) && isErrorType(arg)) && + checkArgumentAssignable(arg, perParamType, argNode) + ) { + return { + value: arg, + node: argNode, + jsValue: resolveDecoratorArgJsValue( + arg, + extractValueOfConstraints({ + kind: "argument", + constraint: perParamType, + }), + jsMarshalling + ), + }; + } else { + return undefined; + } + } for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { const restType = @@ -4697,26 +4794,9 @@ export function createChecker(program: Program): Checker { for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { - const arg = getTypeOrValueForNode(argNode, mapper, { - kind: "argument", - constraint: perParamType, - }); - if ( - arg !== null && - !(isType(arg) && isErrorType(arg)) && - checkArgumentAssignable(arg, perParamType, argNode) - ) { - resolvedArgs.push({ - value: arg, - node: argNode, - jsValue: resolveDecoratorArgJsValue( - arg, - extractValueOfConstraints({ - kind: "argument", - constraint: parameter.type, - }) - ), - }); + const arg = resolveArg(argNode, perParamType); + if (arg) { + resolvedArgs.push(arg); } else { hasError = true; } @@ -4727,26 +4807,9 @@ export function createChecker(program: Program): Checker { } const argNode = node.arguments[index]; if (argNode) { - const arg = getTypeOrValueForNode(argNode, mapper, { - kind: "argument", - constraint: parameter.type, - }); - if ( - arg !== null && - !(isType(arg) && isErrorType(arg)) && - checkArgumentAssignable(arg, parameter.type, argNode) - ) { - resolvedArgs.push({ - value: arg, - node: argNode, - jsValue: resolveDecoratorArgJsValue( - arg, - extractValueOfConstraints({ - kind: "argument", - constraint: parameter.type, - }) - ), - }); + const arg = resolveArg(argNode, parameter.type); + if (arg) { + resolvedArgs.push(arg); } else { hasError = true; } @@ -4761,12 +4824,15 @@ export function createChecker(program: Program): Checker { function resolveDecoratorArgJsValue( value: Type | Value, - valueConstraint: CheckValueConstraint | undefined + valueConstraint: CheckValueConstraint | undefined, + jsMarshalling: "legacy" | "value" ) { if (valueConstraint !== undefined) { if (isValue(value)) { - const [res, diagnostics] = marshallTypeForJSWithLegacyCast(value, valueConstraint.type); - reportCheckerDiagnostics(diagnostics); + const res = + jsMarshalling === "legacy" + ? legacyMarshallTypeForJS(checker, value) + : marshallTypeForJS(value, valueConstraint.type); return res ?? value; } else { return value; @@ -4801,7 +4867,7 @@ export function createChecker(program: Program): Checker { const decorators: DecoratorApplication[] = []; for (const decNode of augmentDecoratorNodes) { - const decorator = checkDecorator(targetType, decNode, mapper); + const decorator = checkDecoratorApplication(targetType, decNode, mapper); if (decorator) { decorators.unshift(decorator); } @@ -4822,7 +4888,7 @@ export function createChecker(program: Program): Checker { ...node.decorators, ]; for (const decNode of decoratorNodes) { - const decorator = checkDecorator(targetType, decNode, mapper); + const decorator = checkDecoratorApplication(targetType, decNode, mapper); if (decorator) { decorators.unshift(decorator); } diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index 02ae5ad72c..bd40432263 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -12,6 +12,7 @@ export { createLinterRule as createRule, createTypeSpecLibrary, defineLinter, + defineModuleFlags, paramMessage, // eslint-disable-next-line deprecation/deprecation setCadlNamespace, diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 8fe4a79a58..ac8de54b0a 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,9 +1,9 @@ +import { Checker } from "./checker.js"; import { compilerAssert } from "./diagnostics.js"; import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; import type { ArrayValue, - Diagnostic, MarshalledValue, NumericValue, ObjectValue, @@ -11,37 +11,59 @@ import type { Value, } from "./types.js"; -/** Legacy version that will cast models to object literals and tuple to tuple literals */ -export function marshallTypeForJSWithLegacyCast( - value: Value, - valueConstraint: Type -): [MarshalledValue | undefined, readonly Diagnostic[]] { - return [marshallTypeForJS(value, valueConstraint) as any, []]; +/** + * Legacy marshalling of values to replicate before 0.56.0 behavior + * - string value -> `string` + * - numeric value -> `number` + * - boolean value -> `boolean` + * - null value -> `NullType` + */ +export function legacyMarshallTypeForJS( + checker: Checker, + value: Value +): Type | Value | Record | unknown[] | string | number | boolean { + switch (value.valueKind) { + case "BooleanValue": + case "StringValue": + return value.value; + case "NumericValue": + return Number(value.value.toString()); + case "ObjectValue": + return objectValueToJs(value); + case "ArrayValue": + return arrayValueToJs(value); + case "EnumValue": + return value.value; + case "NullValue": + return checker.nullType; + case "ScalarValue": + return value; + } } export function marshallTypeForJS( - type: T, + value: T, valueConstraint: Type | undefined ): MarshalledValue { - switch (type.valueKind) { + switch (value.valueKind) { case "BooleanValue": case "StringValue": - return type.value as any; + return value.value as any; case "NumericValue": - return numericValueToJs(type, valueConstraint) as any; + return numericValueToJs(value, valueConstraint) as any; case "ObjectValue": - return objectValueToJs(type) as any; + return objectValueToJs(value) as any; case "ArrayValue": - return arrayValueToJs(type) as any; + return arrayValueToJs(value) as any; case "EnumValue": - return type.value as any; + return value.value as any; case "NullValue": return null as any; case "ScalarValue": - return type as any; + return value as any; } } -function canNumericConstraintBeJsNumber(type: Type | undefined): boolean { +export function canNumericConstraintBeJsNumber(type: Type | undefined): boolean { if (type === undefined) return true; switch (type.kind) { case "Scalar": diff --git a/packages/compiler/src/core/library.ts b/packages/compiler/src/core/library.ts index 4a8842b8fb..8c8ae0b1a1 100644 --- a/packages/compiler/src/core/library.ts +++ b/packages/compiler/src/core/library.ts @@ -7,6 +7,7 @@ import { JSONSchemaValidator, LinterDefinition, LinterRuleDefinition, + ModuleFlags, StateDef, TypeSpecLibrary, TypeSpecLibraryDef, @@ -102,6 +103,10 @@ export function createTypeSpecLibrary< } } +export function defineModuleFlags(flags: ModuleFlags): ModuleFlags { + return flags; +} + export function defineLinter(def: LinterDefinition): LinterDefinition { return def; } diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index c96f234914..6d4a321713 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -553,7 +553,7 @@ export function createProjector( const args: DecoratorArgument[] = []; for (const arg of dec.args) { const jsValue = - typeof arg.jsValue === "object" && "kind" in arg.jsValue + typeof arg.jsValue === "object" && arg.jsValue !== null && "kind" in arg.jsValue ? projectType(arg.jsValue as any) : arg.jsValue; args.push({ ...arg, value: projectType(arg.value), jsValue }); diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 671b084f1e..8f4a81cffc 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -30,13 +30,21 @@ export interface DecoratorArgument { /** * Marshalled value for use in Javascript. */ - jsValue: Type | Value | Record | unknown[] | string | number | boolean | Numeric; + jsValue: + | Type + | Value + | Record + | unknown[] + | string + | number + | boolean + | Numeric + | null; node?: Node; } export interface DecoratorApplication { definition?: Decorator; - // TODO-TIM deprecate replace with `implementation`? decorator: DecoratorFunction; args: DecoratorArgument[]; node?: DecoratorExpressionNode | AugmentDecoratorStatementNode; @@ -1935,23 +1943,29 @@ export type LocationContext = /** Defined in the user project. */ export interface ProjectLocationContext { - type: "project"; + readonly type: "project"; + readonly flags?: ModuleFlags; } /** Built-in */ export interface CompilerLocationContext { - type: "compiler"; + readonly type: "compiler"; } /** Refer to a type that was not declared in a file */ export interface SyntheticLocationContext { - type: "synthetic"; + readonly type: "synthetic"; } /** Defined in a library. */ export interface LibraryLocationContext { - type: "library"; - metadata: ModuleLibraryMetadata; + readonly type: "library"; + + /** Library metadata */ + readonly metadata: ModuleLibraryMetadata; + + /** Module definition */ + readonly flags?: ModuleFlags; } export interface LibraryInstance { @@ -1986,10 +2000,10 @@ export interface FileLibraryMetadata extends LibraryMetadataBase { /** Data for a library. Either loaded via a node_modules package or a standalone js file */ export interface ModuleLibraryMetadata extends LibraryMetadataBase { - type: "module"; + readonly type: "module"; /** Library name as specified in the package.json or in exported $lib. */ - name: string; + readonly name: string; } export interface TextRange { @@ -2325,6 +2339,25 @@ export interface TypeSpecLibraryDef< readonly state?: Record; } +export interface ModuleFlags { + /** + * Decorator arg marshalling algorithm. Specify how TypeSpec values are marshalled to decorator arguments. + * - `value` - New recommended behavior + * - string value -> `string` + * - numeric value -> `number` if the constraint can be represented as a JS number, Numeric otherwise(e.g. for types int64, decimal128, numeric, etc.) + * - boolean value -> `boolean` + * - null value -> `null` + * + * - `legacy` Behavior before version 0.56.0. + * - string value -> `string` + * - numeric value -> `number` + * - boolean value -> `boolean` + * - null value -> `NullType` + * @default legacy + */ + readonly decoratorArgMarshalling?: "legacy" | "value"; +} + export interface LinterDefinition { rules: LinterRuleDefinition[]; ruleSets?: Record; diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index d3b72c30b0..397e35692b 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { setTypeSpecNamespace } from "../../src/core/index.js"; +import { ModuleFlags, isNullType, setTypeSpecNamespace } from "../../src/core/index.js"; import { numericRanges } from "../../src/core/numeric-ranges.js"; import { Numeric } from "../../src/core/numeric.js"; import { @@ -10,6 +10,7 @@ import { createTestWrapper, expectDiagnostics, } from "../../src/testing/index.js"; +import { mutate } from "../../src/utils/misc.js"; describe("compiler: checker: decorators", () => { let testHost: TestHost; @@ -101,14 +102,31 @@ describe("compiler: checker: decorators", () => { message: "Extern declaration must have an implementation in JS file.", }); }); + + describe("emit deprecated warning if decorator is expecting valueof", () => { + it.each(["numeric", "int64", "uint64", "integer", "float", "decimal", "decimal128", "null"])( + "%s", + async (type) => { + const diagnostics = await runner.diagnose(` + extern dec testDec(target: unknown, value: valueof ${type}); + `); + expectDiagnostics(diagnostics, { + code: "deprecated", + }); + } + ); + }); }); describe("usage", () => { let runner: BasicTestRunner; let calledArgs: any[] | undefined; + let $flags: ModuleFlags; beforeEach(() => { + $flags = {}; calledArgs = undefined; testHost.addJsFile("test.js", { + $flags, $testDec: (...args: any[]) => (calledArgs = args), }); runner = createTestWrapper(testHost, { @@ -295,6 +313,7 @@ describe("compiler: checker: decorators", () => { value: string, suppress?: boolean ): Promise { + mutate($flags).decoratorArgMarshalling = "value"; await runner.compile(` extern dec testDec(target: unknown, arg1: ${type}); @@ -386,6 +405,13 @@ describe("compiler: checker: decorators", () => { }); }); + describe("passing null", () => { + it("sends null", async () => { + const arg = await testCallDecorator("valueof null", `null`); + strictEqual(arg, null); + }); + }); + describe("passing an object literal", () => { it("valueof model cast the value to a JS object", async () => { const arg = await testCallDecorator("valueof {name: string}", `#{name: "foo"}`); @@ -445,6 +471,137 @@ describe("compiler: checker: decorators", () => { }); }); }); + + describe("value marshalling (LEGACY)", () => { + async function testCallDecorator( + type: string, + value: string, + suppress?: boolean + ): Promise { + // Default so shouldn't be needed + // mutate($flags).decoratorArgMarshalling = "legacy"; + await runner.compile(` + #suppress "deprecated" "for testing" + extern dec testDec(target: unknown, arg1: ${type}); + + ${suppress ? `#suppress "deprecated" "for testing"` : ""} + @testDec(${value}) + @test + model Foo {} + `); + return calledArgs![2]; + } + + describe("passing a string literal", () => { + it("`: valueof string` cast the value to a JS string", async () => { + const arg = await testCallDecorator("valueof string", `"one"`); + strictEqual(arg, "one"); + }); + + it("`: string` keeps the StringLiteral type", async () => { + const arg = await testCallDecorator("string", `"one"`); + strictEqual(arg.kind, "String"); + }); + }); + + describe("passing a string template", () => { + it("`: valueof string` cast the value to a JS string", async () => { + const arg = await testCallDecorator( + "valueof string", + '"Start ${"one"} middle ${"two"} end"' + ); + strictEqual(arg, "Start one middle two end"); + }); + + it("`: string` keeps the StringTemplate type", async () => { + const arg = await testCallDecorator("string", '"Start ${"one"} middle ${"two"} end"'); + strictEqual(arg.kind, "StringTemplate"); + }); + }); + + describe("passing a numeric literal is always converted to a number", () => { + const explicit: Required> = { + int8: "number", + uint8: "number", + int16: "number", + uint16: "number", + int32: "number", + uint32: "number", + safeint: "number", + float32: "number", + float64: "number", + // Unsafe to convert to JS Number + int64: "number", + uint64: "number", + }; + + const others = [ + ["integer", "number"], + ["numeric", "number"], + ["float", "number"], + ["decimal", "number"], + ["decimal128", "number"], + + // Union of safe numeric + ["int8 | int16", "number", "int8(123)"], + + // Union of unsafe numeric + ["int64 | decimal128", "number", "int8(123)"], + + // Union of safe and unsafe numeric + ["int64 | float64", "number", "int8(123)"], + ]; + + it.each([...Object.entries(explicit), ...others])( + "valueof %s marshal to a %s", + async (type, expectedKind, cstr) => { + const arg = await testCallDecorator(`valueof ${type}`, cstr ?? `123`); + strictEqual(arg, 123); + } + ); + }); + + describe("passing a boolean literal", () => { + it("valueof boolean cast the value to a JS boolean", async () => { + const arg = await testCallDecorator("valueof boolean", `true`); + strictEqual(arg, true); + }); + }); + + describe("passing null", () => { + it("return NullType", async () => { + const arg = await testCallDecorator("valueof null", `null`); + ok(isNullType(arg)); + }); + }); + + describe("passing an object literal", () => { + it("valueof model cast the value to a JS object", async () => { + const arg = await testCallDecorator("valueof {name: string}", `#{name: "foo"}`); + deepStrictEqual(arg, { name: "foo" }); + }); + + it("valueof model cast the value recursively to a JS object", async () => { + const arg = await testCallDecorator( + "valueof {name: unknown}", + `#{name: #{other: "foo"}}` + ); + deepStrictEqual(arg, { name: { other: "foo" } }); + }); + }); + + describe("passing an tuple literal", () => { + it("valueof model cast the value to a JS array", async () => { + const arg = await testCallDecorator("valueof string[]", `#["foo"]`); + deepStrictEqual(arg, ["foo"]); + }); + + it("valueof model cast the value recursively to a JS object", async () => { + const arg = await testCallDecorator("valueof unknown[]", `#[#["foo"]]`); + deepStrictEqual(arg, [["foo"]]); + }); + }); + }); }); it("can have the same name as types", async () => { diff --git a/packages/json-schema/lib/main.tsp b/packages/json-schema/lib/main.tsp index fae17c91d9..78ea6ef8f6 100644 --- a/packages/json-schema/lib/main.tsp +++ b/packages/json-schema/lib/main.tsp @@ -35,6 +35,7 @@ extern dec id(target: unknown, id: valueof string); * * @param value The numeric type must be a multiple of this value. */ +#suppress "deprecated" "" extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); /** diff --git a/packages/json-schema/src/lib.ts b/packages/json-schema/src/lib.ts index d6c87d1e47..5ed8657d9a 100644 --- a/packages/json-schema/src/lib.ts +++ b/packages/json-schema/src/lib.ts @@ -1,4 +1,9 @@ -import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler"; +import { + createTypeSpecLibrary, + defineModuleFlags, + JSONSchemaType, + paramMessage, +} from "@typespec/compiler"; export type FileType = "yaml" | "json"; export type Int64Strategy = "string" | "number"; @@ -80,7 +85,7 @@ export const EmitterOptionsSchema: JSONSchemaType = { required: [], }; -export const libDef = { +export const $lib = createTypeSpecLibrary({ name: "@typespec/json-schema", diagnostics: { "invalid-default": { @@ -105,9 +110,11 @@ export const libDef = { emitter: { options: EmitterOptionsSchema as JSONSchemaType, }, -} as const; +} as const); -export const $lib = createTypeSpecLibrary(libDef); +export const $flags = defineModuleFlags({ + decoratorArgMarshalling: "value", +}); export const { reportDiagnostic, createStateSymbol } = $lib; From b5a80a1515cc4b2a30b15f51155723b7c41ccb1d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 08:48:53 -0700 Subject: [PATCH 101/184] fix null test --- packages/compiler/src/core/checker.ts | 8 +++----- packages/compiler/src/core/js-marshaller.ts | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index f4fb6227de..fd282cf27a 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4829,11 +4829,9 @@ export function createChecker(program: Program): Checker { ) { if (valueConstraint !== undefined) { if (isValue(value)) { - const res = - jsMarshalling === "legacy" - ? legacyMarshallTypeForJS(checker, value) - : marshallTypeForJS(value, valueConstraint.type); - return res ?? value; + return jsMarshalling === "legacy" + ? legacyMarshallTypeForJS(checker, value) + : marshallTypeForJS(value, valueConstraint.type); } else { return value; } diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index ac8de54b0a..03889b7acc 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -40,6 +40,7 @@ export function legacyMarshallTypeForJS( return value; } } + export function marshallTypeForJS( value: T, valueConstraint: Type | undefined From 3b52b6e529afce199aee035ef61092a8563b055f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 09:03:56 -0700 Subject: [PATCH 102/184] add defaultValue tests --- packages/compiler/src/core/types.ts | 1 + packages/compiler/src/lib/decorators.ts | 1 - packages/compiler/test/checker/model.test.ts | 132 +++++++++++++++---- 3 files changed, 105 insertions(+), 29 deletions(-) diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 8f4a81cffc..209f31a261 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -310,6 +310,7 @@ export interface ModelProperty extends BaseType, DecoratedType { // this tracks the property we copied from. sourceProperty?: ModelProperty; optional: boolean; + /** @deprecated use {@link defaultValue} instead. */ default?: Type; defaultValue?: Value; model?: Model; diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 59ae7bf766..54f876b459 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -581,7 +581,6 @@ export function getMaxLength(program: Program, target: Type): number | undefined return getMaxLengthAsNumeric(program, target)?.asNumber() ?? undefined; } -// TODO: better name? /** * Get the maximum length of a string type as a {@link Numeric} value. * @param program Current program diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 5e8f64cbe2..3edf24686d 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -2,7 +2,13 @@ import { deepStrictEqual, match, ok, strictEqual } from "assert"; import { beforeEach, describe, expect, it } from "vitest"; import { isTemplateDeclaration } from "../../src/core/type-utils.js"; import { Model, ModelProperty, Type } from "../../src/core/types.js"; -import { Operation, getDoc, isArrayModelType, isRecordModelType } from "../../src/index.js"; +import { + Numeric, + Operation, + getDoc, + isArrayModelType, + isRecordModelType, +} from "../../src/index.js"; import { TestHost, createTestHost, @@ -103,18 +109,17 @@ describe("compiler: models", () => { ]); }); - // TODO: update to test new defaultValue - describe("assign default values", () => { - const testCases: [string, string, any][] = [ - ["boolean", `false`, { kind: "Boolean", value: false, isFinished: false }], - ["boolean", `true`, { kind: "Boolean", value: true, isFinished: false }], - ["string", `"foo"`, { kind: "String", value: "foo", isFinished: false }], - ["int32", `123`, { kind: "Number", value: 123, valueAsString: "123", isFinished: false }], - ["int32 | null", `null`, { kind: "Intrinsic", name: "null", isFinished: false }], - ]; - - for (const [type, defaultValue, expectedValue] of testCases) { - it(`foo?: ${type} = ${defaultValue}`, async () => { + describe("property defaults", () => { + describe("set defaultValue", () => { + const testCases: [string, string, { kind: string; value: unknown }][] = [ + ["boolean", `false`, { kind: "BooleanValue", value: false }], + ["boolean", `true`, { kind: "BooleanValue", value: true }], + ["string", `"foo"`, { kind: "StringValue", value: "foo" }], + ["int32", `123`, { kind: "NumericValue", value: Numeric("123") }], + ["int32 | null", `null`, { kind: "NullValue", value: null }], + ]; + + it.each(testCases)(`foo?: %s = %s`, async (type, defaultValue, expectedValue) => { testHost.addTypeSpecFile( "main.tsp", ` @@ -122,30 +127,101 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - expect({ ...foo.default }).toMatchObject(expectedValue); + strictEqual(foo.defaultValue?.valueKind, expectedValue.kind); + deepStrictEqual((foo.defaultValue as any).value, expectedValue.value); }); - } - it(`foo?: string[] = #["abc"]`, async () => { - testHost.addTypeSpecFile( - "main.tsp", - ` + it(`foo?: string[] = #["abc"]`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` model A { @test foo?: string[] = #["abc"] } ` - ); - const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - deepStrictEqual(foo.defaultValue?.valueKind, "ArrayValue"); + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + strictEqual(foo.defaultValue?.valueKind, "ArrayValue"); + }); + + it(`foo?: {name: string} = #{name: "abc"}`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: {name: string} = #{name: "abc"} } + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + strictEqual(foo.defaultValue?.valueKind, "ObjectValue"); + }); + + it(`foo?: Enum = Enum.up`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: Enum = Direction.up } + enum Enum {up, down} + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + strictEqual(foo.defaultValue?.valueKind, "EnumValue"); + deepStrictEqual(foo.defaultValue?.value.kind, "EnumMember"); + deepStrictEqual(foo.defaultValue?.value.name, "up"); + }); + + it(`foo?: Union = Union.up`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: Direction = Direction.up } + union Direction {up: "up-value", down: "down-value"} + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + strictEqual(foo.defaultValue?.valueKind, "StringValue"); + deepStrictEqual(foo.defaultValue?.value, "up-value"); + }); }); - it(`foo?: {name: string} = #{name: "abc"}`, async () => { - testHost.addTypeSpecFile( - "main.tsp", + describe("set deprecated default property", () => { + const testCases: [string, string, any][] = [ + ["boolean", `false`, { kind: "Boolean", value: false, isFinished: false }], + ["boolean", `true`, { kind: "Boolean", value: true, isFinished: false }], + ["string", `"foo"`, { kind: "String", value: "foo", isFinished: false }], + ["int32", `123`, { kind: "Number", value: 123, valueAsString: "123", isFinished: false }], + ["int32 | null", `null`, { kind: "Intrinsic", name: "null", isFinished: false }], + ]; + + it.each(testCases)(`foo?: %s = %s`, async (type, defaultValue, expectedValue) => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: ${type} = ${defaultValue} } + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + expect({ ...foo.default }).toMatchObject(expectedValue); + }); + + it(`foo?: string[] = #["abc"] result is not set`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { @test foo?: string[] = #["abc"] } ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + deepStrictEqual(foo.default, undefined); + }); + + it(`foo?: {name: string} = #{name: "abc"} result is not set`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` model A { @test foo?: {name: string} = #{name: "abc"} } ` - ); - const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - deepStrictEqual(foo.defaultValue?.valueKind, "ObjectValue"); + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + deepStrictEqual(foo.default, undefined); + }); }); }); From 0d1d60593333015ca5c772947fdf7f36603de43b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 09:40:46 -0700 Subject: [PATCH 103/184] Add validation for min, max value and length --- packages/compiler/src/core/checker.ts | 57 ++++++++++- packages/compiler/test/checker/model.test.ts | 7 +- .../compiler/test/checker/relation.test.ts | 98 +++++++++++++++++++ .../checker/values/numeric-values.test.ts | 24 +++-- 4 files changed, 174 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index fd282cf27a..2b0ede6d86 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,4 +1,14 @@ -import { $docFromComment, getIndexer, isArrayModelType } from "../lib/decorators.js"; +import { + $docFromComment, + getIndexer, + getMaxLength, + getMaxValueAsNumeric, + getMaxValueExclusiveAsNumeric, + getMinLength, + getMinValueAsNumeric, + getMinValueExclusiveAsNumeric, + isArrayModelType, +} from "../lib/decorators.js"; import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; @@ -6900,7 +6910,7 @@ export function createChecker(program: Program): Checker { return isNumericLiteralRelatedTo(source, target); case "String": case "StringTemplate": - return areScalarsRelated(target, getStdType("string")); + return isStringLiteralRelatedTo(source, target); case "Boolean": return areScalarsRelated(target, getStdType("boolean")); case "Scalar": @@ -6949,7 +6959,28 @@ export function createChecker(program: Program): Checker { } function isNumericLiteralRelatedTo(source: NumericLiteral, target: Scalar) { - return isNumericAssignableToNumericScalar(source.numericValue, target); + // First check that the source numeric literal is assignable to the target scalar + if (!isNumericAssignableToNumericScalar(source.numericValue, target)) { + return false; + } + const min = getMinValueAsNumeric(program, target); + const max = getMaxValueAsNumeric(program, target); + const minExclusive = getMinValueExclusiveAsNumeric(program, target); + const maxExclusive = getMaxValueExclusiveAsNumeric(program, target); + if (min && source.numericValue.lt(min)) { + return false; + } + if (minExclusive && source.numericValue.lte(minExclusive)) { + return false; + } + if (max && source.numericValue.gt(max)) { + return false; + } + + if (maxExclusive && source.numericValue.gte(maxExclusive)) { + return false; + } + return true; } function isNumericAssignableToNumericScalar(source: Numeric, target: Scalar) { @@ -6981,6 +7012,26 @@ export function createChecker(program: Program): Checker { return source.gte(low) && source.lte(high) && (!options.int || isInt); } + function isStringLiteralRelatedTo(source: StringLiteral | StringTemplate, target: Scalar) { + if (!areScalarsRelated(target, getStdType("string"))) { + return false; + } + if (source.kind === "StringTemplate") { + return true; + } + const len = source.value.length; + const min = getMinLength(program, target); + const max = getMaxLength(program, target); + if (min && len < min) { + return false; + } + if (max && len > max) { + return false; + } + + return true; + } + function isModelRelatedTo( source: Model, target: Model, diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 3edf24686d..205fe06110 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -157,8 +157,8 @@ describe("compiler: models", () => { testHost.addTypeSpecFile( "main.tsp", ` - model A { @test foo?: Enum = Direction.up } - enum Enum {up, down} + model A { @test foo?: TestEnum = TestEnum.up } + enum TestEnum {up, down} ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; @@ -198,6 +198,7 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + // eslint-disable-next-line deprecation/deprecation expect({ ...foo.default }).toMatchObject(expectedValue); }); @@ -209,6 +210,7 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + // eslint-disable-next-line deprecation/deprecation deepStrictEqual(foo.default, undefined); }); @@ -220,6 +222,7 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + // eslint-disable-next-line deprecation/deprecation deepStrictEqual(foo.default, undefined); }); }); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 333d7d2d01..ce2ae45560 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -342,6 +342,42 @@ describe("compiler: checker: type relations", () => { }); }); + describe("custom string target", () => { + it("accept string within length", async () => { + await expectTypeAssignable({ + source: `"abcd"`, + target: "myString", + commonCode: `@minLength(3) @maxLength(16) scalar myString extends string;`, + }); + }); + it("validate minValue", async () => { + await expectTypeNotAssignable( + { + source: `"ab"`, + target: "myString", + commonCode: `@minLength(3) scalar myString extends string;`, + }, + { + code: "unassignable", + message: `Type '"ab"' is not assignable to type 'myString'`, + } + ); + }); + it("validate maxValue", async () => { + await expectTypeNotAssignable( + { + source: `"abcdefg"`, + target: "myString", + commonCode: `@maxLength(6) scalar myString extends string;`, + }, + { + code: "unassignable", + message: `Type '"abcdefg"' is not assignable to type 'myString'`, + } + ); + }); + }); + describe("string literal target", () => { it("can the exact same literal", async () => { await expectTypeAssignable({ source: `"foo"`, target: `"foo"` }); @@ -670,6 +706,68 @@ describe("compiler: checker: type relations", () => { }); }); + describe("custom numeric target", () => { + it("accept numeric literal within range", async () => { + await expectTypeAssignable({ + source: "4", + target: "myInt", + commonCode: `@minValue(3) @maxValue(16) scalar myInt extends integer;`, + }); + }); + it("validate minValue", async () => { + await expectTypeNotAssignable( + { + source: "2", + target: "myInt", + commonCode: `@minValue(3) scalar myInt extends integer;`, + }, + { + code: "unassignable", + message: "Type '2' is not assignable to type 'myInt'", + } + ); + }); + it("validate maxValue", async () => { + await expectTypeNotAssignable( + { + source: "16", + target: "myInt", + commonCode: `@maxValue(15) scalar myInt extends integer;`, + }, + { + code: "unassignable", + message: "Type '16' is not assignable to type 'myInt'", + } + ); + }); + it("validate minValueExclusive", async () => { + await expectTypeNotAssignable( + { + source: "3", + target: "myInt", + commonCode: `@minValueExclusive(3) scalar myInt extends integer;`, + }, + { + code: "unassignable", + message: "Type '3' is not assignable to type 'myInt'", + } + ); + }); + it("validate maxValueExclusive", async () => { + await expectTypeNotAssignable( + { + source: "15", + target: "myInt", + commonCode: `@maxValueExclusive(15) scalar myInt extends integer;`, + }, + { + code: "unassignable", + message: "Type '15' is not assignable to type 'myInt'", + } + ); + }); + }); + describe("Record target", () => { ["Record"].forEach((x) => { it(`can assign ${x}`, async () => { diff --git a/packages/compiler/test/checker/values/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts index 54f47a10bb..b33ad9461e 100644 --- a/packages/compiler/test/checker/values/numeric-values.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -1,4 +1,4 @@ -import { ok, strictEqual } from "assert"; +import { strictEqual } from "assert"; import { describe, it } from "vitest"; import { expectDiagnosticEmpty, expectDiagnostics } from "../../../src/testing/expect.js"; import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; @@ -345,11 +345,21 @@ describe("custom numeric scalars", () => { strictEqual(value.value.asNumber(), 2); }); - it.skip("validate value is valid using @minValue and @maxValue", async () => { - const value = await compileValueType( - `int4(2)`, - `@minValue(0) @maxValue(15) scalar uint4 extends integer;` - ); - ok(false); // TODO: implement + describe("using custom min/max values", () => { + const type = `@minValue(0) @maxValue(15) scalar uint4 extends integer;`; + it("accept if value within range", async () => { + const value = await compileValueType(`uint4(2)`, type); + strictEqual(value.valueKind, "NumericValue"); + strictEqual(value.scalar?.name, "uint4"); + strictEqual(value.value.asNumber(), 2); + }); + + it("emit diagnostic if value is out of range", async () => { + const diagnostics = await diagnoseValueType(`uint4(16)`, type); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '16' is not assignable to type 'uint4'", + }); + }); }); }); From 327e4fb576f62e4cbba2bb101951600260466ac1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 10:50:45 -0700 Subject: [PATCH 104/184] rename and fix --- ...ature-object-literals-2024-3-16-10-38-3.md | 24 +++++++++++++++++++ packages/compiler/src/core/checker.ts | 23 ++++++++---------- packages/compiler/src/core/types.ts | 5 ++-- .../compiler/test/checker/decorators.test.ts | 2 +- packages/json-schema/src/index.ts | 2 +- .../json-schema/src/json-schema-emitter.ts | 2 ++ packages/json-schema/src/lib.ts | 2 +- .../decorators-signatures.test.ts | 7 ++++++ 8 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 .chronus/changes/feature-object-literals-2024-3-16-10-38-3.md diff --git a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md new file mode 100644 index 0000000000..e56082e399 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md @@ -0,0 +1,24 @@ +--- +changeKind: deprecation +packages: + - "@typespec/compiler" +--- + +Decorator API: Legacy marshalling logic + + If a library had a decorator with `valueof` one of those types `numeric`, `int64`, `uint64`, `integer`, `float`, `decimal`, `decimal128`, `null` it used to marshall those as JS `number` and `NullType` for `null`. With the introduction of values we have a new marshalling logic which will marshall those numeric types as `Numeric` and the others will remain numbers. `null` will also get marshalled as `null`. + + For now this is an opt-in behavior with a warning on decorators not opt-in having a parameter with a constraint from the list above. + + Example: + ```tsp + extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); + ``` + Will now emit a deprecated warning because `value` is of type `valueof string` which would marshall to `Numeric` under the new logic but as `number` previously. + + To opt-in you can add the following to your library js/ts files. + ```ts + export const $flags = defineModuleFlags({ + decoratorArgMarshalling: "lossless", + }); + ``` diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 2b0ede6d86..696874a566 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1484,12 +1484,6 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined, instantiateTemplates = true ): Type { - // TODO: do we even need this - if (sym.flags & SymbolFlags.Const) { - reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); - return errorType; - } - const result = checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplates); if (result === null || isValue(result)) { reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); @@ -1890,7 +1884,7 @@ export function createChecker(program: Program): Checker { [ `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting null as a type.`, `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "value"}}` to your library.', + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "lossless"}}` to your library.', ].join("\n"), param.node ); @@ -1905,7 +1899,7 @@ export function createChecker(program: Program): Checker { [ `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting a numeric type that is not representable as a JS Number.`, `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "value"}}` to your library.', + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "lossless"}}` to your library.', ].join("\n"), param.node ); @@ -3081,7 +3075,7 @@ export function createChecker(program: Program): Checker { str += stringifyValueForTemplate(typeOrValue); str += span.literal.value; } - return checkStringValue(createLiteralType(str), undefined, node); // TODO: constraint + return checkStringValue(createLiteralType(str), undefined, node); } else { const spans: StringTemplateSpan[] = [createTemplateSpanLiteral(node.head)]; @@ -4517,6 +4511,7 @@ export function createChecker(program: Program): Checker { const defaultValue = checkDefaultValue(prop.default, type.type); if (defaultValue !== null) { type.defaultValue = defaultValue; + // eslint-disable-next-line deprecation/deprecation type.default = checkLegacyDefault(prop.default); } } @@ -4661,11 +4656,13 @@ export function createChecker(program: Program): Checker { }; } - function resolveDecoratorArgMarshalling(declaredType: Decorator | undefined): "value" | "legacy" { + function resolveDecoratorArgMarshalling( + declaredType: Decorator | undefined + ): "lossless" | "legacy" { if (declaredType) { const location = getLocationContext(program, declaredType); if (location.type === "compiler") { - return "value"; + return "lossless"; } else if ( (location.type === "library" || location.type === "project") && location.flags?.decoratorArgMarshalling @@ -4675,7 +4672,7 @@ export function createChecker(program: Program): Checker { return "legacy"; } } - return "value"; + return "lossless"; } /** Check the decorator target is valid */ @@ -4835,7 +4832,7 @@ export function createChecker(program: Program): Checker { function resolveDecoratorArgJsValue( value: Type | Value, valueConstraint: CheckValueConstraint | undefined, - jsMarshalling: "legacy" | "value" + jsMarshalling: "legacy" | "lossless" ) { if (valueConstraint !== undefined) { if (isValue(value)) { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 209f31a261..68f22a1703 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -408,7 +408,6 @@ export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { symbol?: Sym; } -// TODO: should we just call that Constructor or NamedConstructor for future proof? export interface ScalarConstructor extends BaseType { kind: "ScalarConstructor"; node: ScalarConstructorNode; @@ -2343,7 +2342,7 @@ export interface TypeSpecLibraryDef< export interface ModuleFlags { /** * Decorator arg marshalling algorithm. Specify how TypeSpec values are marshalled to decorator arguments. - * - `value` - New recommended behavior + * - `lossless` - New recommended behavior * - string value -> `string` * - numeric value -> `number` if the constraint can be represented as a JS number, Numeric otherwise(e.g. for types int64, decimal128, numeric, etc.) * - boolean value -> `boolean` @@ -2356,7 +2355,7 @@ export interface ModuleFlags { * - null value -> `NullType` * @default legacy */ - readonly decoratorArgMarshalling?: "legacy" | "value"; + readonly decoratorArgMarshalling?: "legacy" | "lossless"; } export interface LinterDefinition { diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 397e35692b..f488d3b4ba 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -313,7 +313,7 @@ describe("compiler: checker: decorators", () => { value: string, suppress?: boolean ): Promise { - mutate($flags).decoratorArgMarshalling = "value"; + mutate($flags).decoratorArgMarshalling = "lossless"; await runner.compile(` extern dec testDec(target: unknown, arg1: ${type}); diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 33ccc7d8a8..de5c4bdced 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -36,7 +36,7 @@ import { JsonSchemaEmitter } from "./json-schema-emitter.js"; import { JSONSchemaEmitterOptions, createStateSymbol } from "./lib.js"; export { JsonSchemaEmitter } from "./json-schema-emitter.js"; -export { $lib, EmitterOptionsSchema, JSONSchemaEmitterOptions } from "./lib.js"; +export { $flags, $lib, EmitterOptionsSchema, JSONSchemaEmitterOptions } from "./lib.js"; export const namespace = "TypeSpec.JsonSchema"; export type JsonSchemaDeclaration = Model | Union | Enum | Scalar; diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index eaec06d930..781b861ab1 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -172,7 +172,9 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche const result = new ObjectBuilder(propertyType.value); + // eslint-disable-next-line deprecation/deprecation if (property.default) { + // eslint-disable-next-line deprecation/deprecation result.default = this.#getDefaultValue(property.type, property.default); } diff --git a/packages/json-schema/src/lib.ts b/packages/json-schema/src/lib.ts index 5ed8657d9a..c11be6b0fe 100644 --- a/packages/json-schema/src/lib.ts +++ b/packages/json-schema/src/lib.ts @@ -113,7 +113,7 @@ export const $lib = createTypeSpecLibrary({ } as const); export const $flags = defineModuleFlags({ - decoratorArgMarshalling: "value", + decoratorArgMarshalling: "lossless", }); export const { reportDiagnostic, createStateSymbol } = $lib; diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index 42d2f412b8..434e5858cd 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -1,3 +1,4 @@ +import { defineModuleFlags } from "@typespec/compiler"; import { createTestHost, expectDiagnosticEmpty } from "@typespec/compiler/testing"; import { describe, expect, it } from "vitest"; import { generateExternDecorators } from "../../src/gen-extern-signatures/gen-extern-signatures.js"; @@ -7,9 +8,15 @@ async function generateDecoratorSignatures(code: string) { host.addTypeSpecFile( "main.tsp", ` + import "./lib.js"; using TypeSpec.Reflection; ${code}` ); + host.addJsFile("lib.js", { + $flags: defineModuleFlags({ + decoratorArgMarshalling: "lossless", + }), + }); await host.diagnose("main.tsp", { parseOptions: { comments: true, docs: true }, }); From 483f27ccf72a9c88519ee20e0e1178509020af27 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 11:03:31 -0700 Subject: [PATCH 105/184] Create feature-object-literals-2024-3-16-17-54-23.md --- .../changes/feature-object-literals-2024-3-16-17-54-23.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/feature-object-literals-2024-3-16-17-54-23.md diff --git a/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md b/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md new file mode 100644 index 0000000000..7cb1e20f75 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/html-program-viewer" +--- + +Add support for new fields added with the value world From 55a148869fbc6984884d74eb77234540159a8f78 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 11:04:53 -0700 Subject: [PATCH 106/184] fix lint --- packages/compiler/src/lib/decorators.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 54f876b459..fd3a050abb 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -1056,7 +1056,11 @@ export const $withoutDefaultValues: WithoutDefaultValuesDecorator = ( target: Model ) => { // remove all read-only properties from the target type - target.properties.forEach((p) => delete p.default); + target.properties.forEach((p) => { + // eslint-disable-next-line deprecation/deprecation + delete p.default; + delete p.defaultValue; + }); }; // -- @list decorator --------------------- From da8e037414c23009318e30198b9b78e5476e38f5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 11:18:21 -0700 Subject: [PATCH 107/184] Add string template tests --- .../test/checker/string-template.test.ts | 127 +++++++++++------- 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/packages/compiler/test/checker/string-template.test.ts b/packages/compiler/test/checker/string-template.test.ts index 2f0a068032..2ef1f1f018 100644 --- a/packages/compiler/test/checker/string-template.test.ts +++ b/packages/compiler/test/checker/string-template.test.ts @@ -1,74 +1,103 @@ import { strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { Model, StringTemplate } from "../../src/index.js"; -import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; +import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.js"; -describe("compiler: string templates", () => { - let runner: BasicTestRunner; +let runner: BasicTestRunner; - beforeEach(async () => { - runner = await createTestRunner(); - }); +beforeEach(async () => { + runner = await createTestRunner(); +}); - async function compileStringTemplate( - templateString: string, - other?: string - ): Promise { - const { Test } = (await runner.compile( - ` +async function compileStringTemplate( + templateString: string, + other?: string +): Promise { + const { Test } = (await runner.compile( + ` @test model Test { test: ${templateString}; } ${other ?? ""} ` - )) as { Test: Model }; + )) as { Test: Model }; - const prop = Test.properties.get("test")!.type; + const prop = Test.properties.get("test")!.type; - strictEqual(prop.kind, "StringTemplate"); - return prop; - } + strictEqual(prop.kind, "StringTemplate"); + return prop; +} - it("simple", async () => { - const template = await compileStringTemplate(`"Start \${123} end"`); - strictEqual(template.spans.length, 3); - strictEqual(template.spans[0].isInterpolated, false); - strictEqual(template.spans[0].type.value, "Start "); +it("simple", async () => { + const template = await compileStringTemplate(`"Start \${123} end"`); + strictEqual(template.spans.length, 3); + strictEqual(template.spans[0].isInterpolated, false); + strictEqual(template.spans[0].type.value, "Start "); - strictEqual(template.spans[1].isInterpolated, true); - strictEqual(template.spans[1].type.kind, "Number"); - strictEqual(template.spans[1].type.value, 123); + strictEqual(template.spans[1].isInterpolated, true); + strictEqual(template.spans[1].type.kind, "Number"); + strictEqual(template.spans[1].type.value, 123); - strictEqual(template.spans[2].isInterpolated, false); - strictEqual(template.spans[2].type.value, " end"); - }); + strictEqual(template.spans[2].isInterpolated, false); + strictEqual(template.spans[2].type.value, " end"); +}); - it("string interpolated are marked with isInterpolated", async () => { - const template = await compileStringTemplate(`"Start \${"interpolate"} end"`); - strictEqual(template.spans.length, 3); - strictEqual(template.spans[0].isInterpolated, false); - strictEqual(template.spans[0].type.value, "Start "); +it("string interpolated are marked with isInterpolated", async () => { + const template = await compileStringTemplate(`"Start \${"interpolate"} end"`); + strictEqual(template.spans.length, 3); + strictEqual(template.spans[0].isInterpolated, false); + strictEqual(template.spans[0].type.value, "Start "); - strictEqual(template.spans[1].isInterpolated, true); - strictEqual(template.spans[1].type.kind, "String"); - strictEqual(template.spans[1].type.value, "interpolate"); + strictEqual(template.spans[1].isInterpolated, true); + strictEqual(template.spans[1].type.kind, "String"); + strictEqual(template.spans[1].type.value, "interpolate"); - strictEqual(template.spans[2].isInterpolated, false); - strictEqual(template.spans[2].type.value, " end"); - }); + strictEqual(template.spans[2].isInterpolated, false); + strictEqual(template.spans[2].type.value, " end"); +}); + +it("can interpolate a model", async () => { + const template = await compileStringTemplate(`"Start \${TestModel} end"`, "model TestModel {}"); + strictEqual(template.spans.length, 3); + strictEqual(template.spans[0].isInterpolated, false); + strictEqual(template.spans[0].type.value, "Start "); + + strictEqual(template.spans[1].isInterpolated, true); + strictEqual(template.spans[1].type.kind, "Model"); + strictEqual(template.spans[1].type.name, "TestModel"); - it("can interpolate a model", async () => { - const template = await compileStringTemplate(`"Start \${TestModel} end"`, "model TestModel {}"); - strictEqual(template.spans.length, 3); - strictEqual(template.spans[0].isInterpolated, false); - strictEqual(template.spans[0].type.value, "Start "); + strictEqual(template.spans[2].isInterpolated, false); + strictEqual(template.spans[2].type.value, " end"); +}); - strictEqual(template.spans[1].isInterpolated, true); - strictEqual(template.spans[1].type.kind, "Model"); - strictEqual(template.spans[1].type.name, "TestModel"); +it("emit error if interpolating value and types", async () => { + const diagnostics = await runner.diagnose( + ` + const str1 = "hi"; + alias str2 = "\${str1} and \${string}"; + ` + ); + expectDiagnostics(diagnostics, { + code: "mixed-string-template", + message: + "String template is interpolating values and types. It must be either all values to produce a string value or or all types for string template type.", + }); +}); - strictEqual(template.spans[2].isInterpolated, false); - strictEqual(template.spans[2].type.value, " end"); +describe("emit error if interpolating value in a context where template is used as a type", () => { + it.each([ + ["alias", `alias str2 = "with value \${str1}";`], + ["model prop", `model Foo { a: "with value \${str1}"; }`], + ])("%s", async (_, code) => { + const source = ` + const str1 = "hi"; + ${code} + `; + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + }); }); }); From 212533cb7a9c9153d727591e05dcd21a5d9e6767 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 11:56:07 -0700 Subject: [PATCH 108/184] Add valueof casting tests --- .../test/checker/valueof-casting.test.ts | 85 +++++++++++++++++++ .../test/checker/values/array-values.test.ts | 12 +-- .../checker/values/boolean-values.test.ts | 12 +-- .../test/checker/values/const.test.ts | 6 +- .../checker/values/numeric-values.test.ts | 20 ++--- .../test/checker/values/object-values.test.ts | 28 +++--- .../test/checker/values/scalar-values.test.ts | 26 +++--- .../test/checker/values/string-values.test.ts | 25 +++--- .../compiler/test/checker/values/utils.ts | 13 ++- 9 files changed, 150 insertions(+), 77 deletions(-) create mode 100644 packages/compiler/test/checker/valueof-casting.test.ts diff --git a/packages/compiler/test/checker/valueof-casting.test.ts b/packages/compiler/test/checker/valueof-casting.test.ts new file mode 100644 index 0000000000..c9eb7c8c04 --- /dev/null +++ b/packages/compiler/test/checker/valueof-casting.test.ts @@ -0,0 +1,85 @@ +import { ok, strictEqual } from "assert"; +import { it } from "vitest"; +import { Diagnostic, Model, Type, Value, isType, isValue } from "../../src/index.js"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/expect.js"; +import { createTestHost } from "../../src/testing/test-host.js"; + +export async function compileAndDiagnoseValueOrType( + constraint: string, + code: string, + other?: string +): Promise<[Type | Value | undefined, readonly Diagnostic[]]> { + const host = await createTestHost(); + host.addTypeSpecFile( + "main.tsp", + ` + @test model Test {} + alias Instance = Test<${code}>; + + ${other ?? ""} + ` + ); + const [{ Test }, diagnostics] = await host.compileAndDiagnose("main.tsp"); + const arg = (Test as Model).templateMapper?.args[0]; + return [arg, diagnostics]; +} + +export async function compileValueOrType( + constraint: string, + code: string, + other?: string +): Promise { + const [called, diagnostics] = await compileAndDiagnoseValueOrType(constraint, code, other); + expectDiagnosticEmpty(diagnostics); + ok(called, "Decorator was not called"); + + return called; +} + +export async function diagnoseValueOrType( + constraint: string, + code: string, + other?: string +): Promise { + const [_, diagnostics] = await compileAndDiagnoseValueOrType(constraint, code, other); + return diagnostics; +} + +it("extends valueof string returns a string value", async () => { + const entity = await compileValueOrType("valueof string", `"hello"`); + ok(isValue(entity)); + strictEqual(entity.valueKind, "StringValue"); +}); + +it("extends valueof int32 returns a numeric value", async () => { + const entity = await compileValueOrType("valueof int32", `123`); + ok(isValue(entity)); + strictEqual(entity.valueKind, "NumericValue"); +}); + +it("extends string returns a string literal type", async () => { + const entity = await compileValueOrType("string", `"hello"`); + ok(isType(entity)); + strictEqual(entity.kind, "String"); +}); + +it("extends int32 returns a numeric literal type", async () => { + const entity = await compileValueOrType("int32", `123`); + ok(isType(entity)); + strictEqual(entity.kind, "Number"); +}); + +it("value wins over type if both are accepted", async () => { + const entity = await compileValueOrType("(valueof string) | string", `"hello"`); + ok(isValue(entity)); + strictEqual(entity.valueKind, "StringValue"); +}); + +it("ambiguous valueof with type option still emit ambiguous error", async () => { + const diagnostics = await diagnoseValueOrType("(valueof int32 | int64) | int32", `123`); + expectDiagnostics(diagnostics, { + code: "ambiguous-scalar-type", + message: + "Value 123 type is ambiguous between int32, int64. To resolve be explicit when instantiating this value(e.g. 'int32(123)').", + }); +}); diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index 35b7510e92..50525d35f1 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -2,16 +2,16 @@ import { ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { Model, isValue } from "../../../src/index.js"; import { createTestRunner, expectDiagnostics } from "../../../src/testing/index.js"; -import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; +import { compileValue, diagnoseUsage, diagnoseValue } from "./utils.js"; it("no values", async () => { - const object = await compileValueType(`#[]`); + const object = await compileValue(`#[]`); strictEqual(object.valueKind, "ArrayValue"); strictEqual(object.values.length, 0); }); it("single value", async () => { - const object = await compileValueType(`#["John"]`); + const object = await compileValue(`#["John"]`); strictEqual(object.valueKind, "ArrayValue"); strictEqual(object.values.length, 1); const first = object.values[0]; @@ -20,7 +20,7 @@ it("single value", async () => { }); it("multiple property", async () => { - const object = await compileValueType(`#["John", 21]`); + const object = await compileValue(`#["John", 21]`); strictEqual(object.valueKind, "ArrayValue"); strictEqual(object.values.length, 2); @@ -43,7 +43,7 @@ describe("valid property types", () => { ["ObjectValue", `#{nested: "foo"}`], ["ArrayValue", `#["foo"]`], ])("%s", async (kind, type, other?) => { - const object = await compileValueType(`#[${type}]`, other); + const object = await compileValue(`#[${type}]`, other); strictEqual(object.valueKind, "ArrayValue"); const nameProp = object.values[0]; strictEqual(nameProp?.valueKind, kind); @@ -51,7 +51,7 @@ describe("valid property types", () => { }); it("emit diagnostic if referencing a non literal type", async () => { - const diagnostics = await diagnoseValueType(`#[{ thisIsAModel: true }]`); + const diagnostics = await diagnoseValue(`#[{ thisIsAModel: true }]`); expectDiagnostics(diagnostics, { code: "expect-value", message: "{ thisIsAModel: true } refers to a type, but is being used as a value here.", diff --git a/packages/compiler/test/checker/values/boolean-values.test.ts b/packages/compiler/test/checker/values/boolean-values.test.ts index 24fa3eae1c..dd5a593425 100644 --- a/packages/compiler/test/checker/values/boolean-values.test.ts +++ b/packages/compiler/test/checker/values/boolean-values.test.ts @@ -1,11 +1,11 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; import { expectDiagnostics } from "../../../src/testing/expect.js"; -import { compileValueType, diagnoseValueType } from "./utils.js"; +import { compileValue, diagnoseValue } from "./utils.js"; describe("instantiate with constructor", () => { it("with boolean literal", async () => { - const value = await compileValueType(`boolean(true)`); + const value = await compileValue(`boolean(true)`); strictEqual(value.valueKind, "BooleanValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "boolean"); @@ -16,7 +16,7 @@ describe("instantiate with constructor", () => { describe("implicit type", () => { it("doesn't pick scalar if const has no type", async () => { - const value = await compileValueType(`a`, `const a = true;`); + const value = await compileValue(`a`, `const a = true;`); strictEqual(value.valueKind, "BooleanValue"); strictEqual(value.type.kind, "Boolean"); strictEqual(value.type.value, true); @@ -25,7 +25,7 @@ describe("implicit type", () => { }); it("instantiate if there is a single string option", async () => { - const value = await compileValueType(`a`, `const a: boolean | string = true;`); + const value = await compileValue(`a`, `const a: boolean | string = true;`); strictEqual(value.valueKind, "BooleanValue"); strictEqual(value.type.kind, "Union"); strictEqual(value.scalar?.name, "boolean"); @@ -33,7 +33,7 @@ describe("implicit type", () => { }); it("emit diagnostics if there is multiple numeric choices", async () => { - const diagnostics = await diagnoseValueType( + const diagnostics = await diagnoseValue( `a`, ` const a: boolean | myBoolean = true; @@ -61,7 +61,7 @@ describe("validate literal are assignable", () => { describe.each(cases)("%s", (scalarName, values) => { it.each(values)(`%s %s`, async (expected, value, message) => { - const diagnostics = await diagnoseValueType(`${scalarName}(${value})`); + const diagnostics = await diagnoseValue(`${scalarName}(${value})`); expectDiagnostics(diagnostics, expected === "✔" ? [] : [{ message: message ?? "" }]); }); }); diff --git a/packages/compiler/test/checker/values/const.test.ts b/packages/compiler/test/checker/values/const.test.ts index 97633c08d7..41e7d48024 100644 --- a/packages/compiler/test/checker/values/const.test.ts +++ b/packages/compiler/test/checker/values/const.test.ts @@ -1,7 +1,7 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; import { expectDiagnostics } from "../../../src/testing/expect.js"; -import { compileValueType, diagnoseUsage } from "./utils.js"; +import { compileValue, diagnoseUsage } from "./utils.js"; describe("without type it use the most precise type", () => { it.each([ @@ -11,13 +11,13 @@ describe("without type it use the most precise type", () => { [`#{foo: "abc"}`, "Model"], [`#["abc"]`, "Tuple"], ])("%s => %s", async (input, kind) => { - const value = await compileValueType("a", `const a = ${input};`); + const value = await compileValue("a", `const a = ${input};`); strictEqual(value.type.kind, kind); }); }); it("when assigning another const it change the type", async () => { - const value = await compileValueType("b", `const a: int32 = 123;const b: int64 = a;`); + const value = await compileValue("b", `const a: int32 = 123;const b: int64 = a;`); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "int64"); }); diff --git a/packages/compiler/test/checker/values/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts index b33ad9461e..9296ecafb1 100644 --- a/packages/compiler/test/checker/values/numeric-values.test.ts +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -1,7 +1,7 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; import { expectDiagnosticEmpty, expectDiagnostics } from "../../../src/testing/expect.js"; -import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; +import { compileValue, diagnoseUsage, diagnoseValue } from "./utils.js"; const numericScalars = [ "numeric", @@ -27,7 +27,7 @@ const numericScalars = [ describe("instantiate with constructor", () => { it.each(numericScalars)("%s", async (scalarName) => { - const value = await compileValueType(`${scalarName}(123)`); + const value = await compileValue(`${scalarName}(123)`); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, scalarName); @@ -39,7 +39,7 @@ describe("instantiate with constructor", () => { describe("implicit type", () => { describe("instantiate when type is scalar", () => { it.each(numericScalars)("%s", async (scalarName) => { - const value = await compileValueType(`a`, `const a:${scalarName} = 123;`); + const value = await compileValue(`a`, `const a:${scalarName} = 123;`); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, scalarName); @@ -49,7 +49,7 @@ describe("implicit type", () => { }); it("doesn't pick scalar if const has no type", async () => { - const value = await compileValueType(`a`, `const a = 123;`); + const value = await compileValue(`a`, `const a = 123;`); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.type.kind, "Number"); strictEqual(value.type.valueAsString, "123"); @@ -58,7 +58,7 @@ describe("implicit type", () => { }); it("instantiate if there is a single numeric option", async () => { - const value = await compileValueType(`a`, `const a: int32 | string = 123;`); + const value = await compileValue(`a`, `const a: int32 | string = 123;`); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.type.kind, "Union"); strictEqual(value.scalar?.name, "int32"); @@ -66,7 +66,7 @@ describe("implicit type", () => { }); it("emit diagnostics if there is multiple numeric choices", async () => { - const diagnostics = await diagnoseValueType(`a`, `const a: int32 | int64 = 123;`); + const diagnostics = await diagnoseValue(`a`, `const a: int32 | int64 = 123;`); expectDiagnostics(diagnostics, { code: "ambiguous-scalar-type", message: @@ -281,7 +281,7 @@ describe("instantiate from another smaller numeric type", () => { ["uint32", "integer"], ["uint32", "numeric"], ])("%s → %s", async (a, b) => { - const value = await compileValueType(`${b}(${a}(123))`); + const value = await compileValue(`${b}(${a}(123))`); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.scalar?.name, b); strictEqual(value.type.kind, "Scalar"); @@ -337,7 +337,7 @@ describe("cannot instantiate from a larger numeric type", () => { describe("custom numeric scalars", () => { it("instantiates a custom scalar", async () => { - const value = await compileValueType(`int4(2)`, "scalar int4 extends integer;"); + const value = await compileValue(`int4(2)`, "scalar int4 extends integer;"); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "int4"); @@ -348,14 +348,14 @@ describe("custom numeric scalars", () => { describe("using custom min/max values", () => { const type = `@minValue(0) @maxValue(15) scalar uint4 extends integer;`; it("accept if value within range", async () => { - const value = await compileValueType(`uint4(2)`, type); + const value = await compileValue(`uint4(2)`, type); strictEqual(value.valueKind, "NumericValue"); strictEqual(value.scalar?.name, "uint4"); strictEqual(value.value.asNumber(), 2); }); it("emit diagnostic if value is out of range", async () => { - const diagnostics = await diagnoseValueType(`uint4(16)`, type); + const diagnostics = await diagnoseValue(`uint4(16)`, type); expectDiagnostics(diagnostics, { code: "unassignable", message: "Type '16' is not assignable to type 'uint4'", diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index d3071a52a4..55e25cfbab 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -2,16 +2,16 @@ import { ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { Model, isValue } from "../../../src/index.js"; import { createTestRunner, expectDiagnostics } from "../../../src/testing/index.js"; -import { compileValueType, diagnoseUsage, diagnoseValueType } from "./utils.js"; +import { compileValue, diagnoseUsage, diagnoseValue } from "./utils.js"; it("no properties", async () => { - const object = await compileValueType(`#{}`); + const object = await compileValue(`#{}`); strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 0); }); it("single property", async () => { - const object = await compileValueType(`#{name: "John"}`); + const object = await compileValue(`#{name: "John"}`); strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 1); const nameProp = object.properties.get("name")?.value; @@ -20,7 +20,7 @@ it("single property", async () => { }); it("multiple property", async () => { - const object = await compileValueType(`#{name: "John", age: 21}`); + const object = await compileValue(`#{name: "John", age: 21}`); strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 2); @@ -35,10 +35,7 @@ it("multiple property", async () => { describe("spreading", () => { it("add the properties", async () => { - const object = await compileValueType( - `#{...Common, age: 21}`, - `const Common = #{ name: "John" };` - ); + const object = await compileValue(`#{...Common, age: 21}`, `const Common = #{ name: "John" };`); strictEqual(object.valueKind, "ObjectValue"); strictEqual(object.properties.size, 2); @@ -52,7 +49,7 @@ describe("spreading", () => { }); it("override properties defined before if there is a name conflict", async () => { - const object = await compileValueType( + const object = await compileValue( `#{name: "John", age: 21, ...Common, }`, `const Common = #{ name: "Common" };` ); @@ -64,7 +61,7 @@ describe("spreading", () => { }); it("override properties spread before", async () => { - const object = await compileValueType( + const object = await compileValue( `#{...Common, name: "John", age: 21 }`, `const Common = #{ name: "John" };` ); @@ -76,7 +73,7 @@ describe("spreading", () => { }); it("emit diagnostic is spreading a model", async () => { - const diagnostics = await diagnoseValueType( + const diagnostics = await diagnoseValue( `#{...Common, age: 21}`, `alias Common = { name: "John" };` ); @@ -87,10 +84,7 @@ describe("spreading", () => { }); it("emit diagnostic is spreading a non-object values", async () => { - const diagnostics = await diagnoseValueType( - `#{...Common, age: 21}`, - `const Common = #["abc"];` - ); + const diagnostics = await diagnoseValue(`#{...Common, age: 21}`, `const Common = #["abc"];`); expectDiagnostics(diagnostics, { code: "spread-object", message: "Cannot spread properties of non-object type.", @@ -108,7 +102,7 @@ describe("valid property types", () => { ["ObjectValue", `#{nested: "foo"}`], ["ArrayValue", `#["foo"]`], ])("%s", async (kind, type, other?) => { - const object = await compileValueType(`#{prop: ${type}}`, other); + const object = await compileValue(`#{prop: ${type}}`, other); strictEqual(object.valueKind, "ObjectValue"); const nameProp = object.properties.get("prop")?.value; strictEqual(nameProp?.valueKind, kind); @@ -116,7 +110,7 @@ describe("valid property types", () => { }); it("emit diagnostic if referencing a non literal type", async () => { - const diagnostics = await diagnoseValueType(`#{ prop: { thisIsAModel: true }}`); + const diagnostics = await diagnoseValue(`#{ prop: { thisIsAModel: true }}`); expectDiagnostics(diagnostics, { code: "expect-value", message: "{ thisIsAModel: true } refers to a type, but is being used as a value here.", diff --git a/packages/compiler/test/checker/values/scalar-values.test.ts b/packages/compiler/test/checker/values/scalar-values.test.ts index d1787ad060..fc5315ae5b 100644 --- a/packages/compiler/test/checker/values/scalar-values.test.ts +++ b/packages/compiler/test/checker/values/scalar-values.test.ts @@ -1,7 +1,7 @@ import { strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { expectDiagnostics } from "../../../src/testing/expect.js"; -import { compileValueType, diagnoseValueType } from "./utils.js"; +import { compileValue, diagnoseValue } from "./utils.js"; describe("instantiate with named constructor", () => { const ipv4Code = ` @@ -12,7 +12,7 @@ describe("instantiate with named constructor", () => { `; it("with single arg", async () => { - const value = await compileValueType(`ipv4.fromString("0.0.1.1")`, ipv4Code); + const value = await compileValue(`ipv4.fromString("0.0.1.1")`, ipv4Code); strictEqual(value.valueKind, "ScalarValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "ipv4"); @@ -27,7 +27,7 @@ describe("instantiate with named constructor", () => { }); it("with multiple args", async () => { - const value = await compileValueType(`ipv4.fromBytes(0, 0, 1, 1)`, ipv4Code); + const value = await compileValue(`ipv4.fromBytes(0, 0, 1, 1)`, ipv4Code); strictEqual(value.valueKind, "ScalarValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "ipv4"); @@ -50,7 +50,7 @@ describe("instantiate with named constructor", () => { }); it("instantiate from another scalar", async () => { - const value = await compileValueType( + const value = await compileValue( `b.fromA(a.fromString("a"))`, ` scalar a { init fromString(val: string);} @@ -70,7 +70,7 @@ describe("instantiate with named constructor", () => { }); it("emit warning if passing wrong type to constructor", async () => { - const diagnostics = await diagnoseValueType(`ipv4.fromString(123)`, ipv4Code); + const diagnostics = await diagnoseValue(`ipv4.fromString(123)`, ipv4Code); expectDiagnostics(diagnostics, { code: "invalid-argument", message: "Argument '123' is not assignable to parameter of type 'string'", @@ -78,7 +78,7 @@ describe("instantiate with named constructor", () => { }); it("emit warning if passing too many args", async () => { - const diagnostics = await diagnoseValueType(`ipv4.fromString("abc", "def")`, ipv4Code); + const diagnostics = await diagnoseValue(`ipv4.fromString("abc", "def")`, ipv4Code); expectDiagnostics(diagnostics, { code: "invalid-argument-count", message: "Expected 1 arguments, but got 2.", @@ -86,7 +86,7 @@ describe("instantiate with named constructor", () => { }); it("emit warning if passing too few args", async () => { - const diagnostics = await diagnoseValueType(`ipv4.fromBytes(0, 0, 0)`, ipv4Code); + const diagnostics = await diagnoseValue(`ipv4.fromBytes(0, 0, 0)`, ipv4Code); expectDiagnostics(diagnostics, { code: "invalid-argument-count", message: "Expected 4 arguments, but got 3.", @@ -95,7 +95,7 @@ describe("instantiate with named constructor", () => { describe("with optional params", () => { it("allow not providing it", async () => { - const value = await compileValueType( + const value = await compileValue( `ipv4.fromItems("a")`, ` scalar ipv4 { @@ -108,7 +108,7 @@ describe("instantiate with named constructor", () => { expect(value.value.args).toHaveLength(1); }); it("allow providing it", async () => { - const value = await compileValueType( + const value = await compileValue( `ipv4.fromItems("a", "b")`, ` scalar ipv4 { @@ -122,7 +122,7 @@ describe("instantiate with named constructor", () => { }); it("emit warning if passing wrong type to constructor", async () => { - const diagnostics = await diagnoseValueType( + const diagnostics = await diagnoseValue( `ipv4.fromItems("a", 123)`, ` scalar ipv4 { @@ -138,7 +138,7 @@ describe("instantiate with named constructor", () => { }); describe("with rest params", () => { it("support rest params", async () => { - const value = await compileValueType( + const value = await compileValue( `ipv4.fromItems("a", "b", "c")`, ` scalar ipv4 { @@ -152,7 +152,7 @@ describe("instantiate with named constructor", () => { }); it("support rest params with positional before", async () => { - const value = await compileValueType( + const value = await compileValue( `ipv4.fromItems(1, "b", "c")`, ` scalar ipv4 { @@ -166,7 +166,7 @@ describe("instantiate with named constructor", () => { }); it("emit warning if passing wrong type to constructor", async () => { - const diagnostics = await diagnoseValueType( + const diagnostics = await diagnoseValue( `ipv4.fromItems(123)`, ` scalar ipv4 { diff --git a/packages/compiler/test/checker/values/string-values.test.ts b/packages/compiler/test/checker/values/string-values.test.ts index 9f3b2f94aa..0ae55b3fc9 100644 --- a/packages/compiler/test/checker/values/string-values.test.ts +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -1,11 +1,11 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; import { expectDiagnostics } from "../../../src/testing/expect.js"; -import { compileValueType, diagnoseValueType } from "./utils.js"; +import { compileValue, diagnoseValue } from "./utils.js"; describe("instantiate with constructor", () => { it("string", async () => { - const value = await compileValueType(`string("abc")`); + const value = await compileValue(`string("abc")`); strictEqual(value.valueKind, "StringValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "string"); @@ -16,7 +16,7 @@ describe("instantiate with constructor", () => { describe("implicit type", () => { it("doesn't pick scalar if const has no type (string literal)", async () => { - const value = await compileValueType(`a`, `const a = "abc";`); + const value = await compileValue(`a`, `const a = "abc";`); strictEqual(value.valueKind, "StringValue"); strictEqual(value.type.kind, "String"); strictEqual(value.type.value, "abc"); @@ -24,7 +24,7 @@ describe("implicit type", () => { strictEqual(value.value, "abc"); }); it("doesn't pick scalar if const has no type (string template )", async () => { - const value = await compileValueType(`a`, `const a = "one ${"abc"} def";`); + const value = await compileValue(`a`, `const a = "one ${"abc"} def";`); strictEqual(value.valueKind, "StringValue"); strictEqual(value.type.kind, "String"); strictEqual(value.type.value, "one abc def"); @@ -33,7 +33,7 @@ describe("implicit type", () => { }); it("instantiate if there is a single string option", async () => { - const value = await compileValueType(`a`, `const a: int32 | string = "abc";`); + const value = await compileValue(`a`, `const a: int32 | string = "abc";`); strictEqual(value.valueKind, "StringValue"); strictEqual(value.type.kind, "Union"); strictEqual(value.scalar?.name, "string"); @@ -41,7 +41,7 @@ describe("implicit type", () => { }); it("emit diagnostics if there is multiple numeric choices", async () => { - const diagnostics = await diagnoseValueType( + const diagnostics = await diagnoseValue( `a`, ` const a: string | myString = "abc"; @@ -56,7 +56,7 @@ describe("implicit type", () => { describe("string templates", () => { it("create string value from string template if able to serialize to string", async () => { - const value = await compileValueType(`string("one \${"abc"} def")`); + const value = await compileValue(`string("one \${"abc"} def")`); strictEqual(value.valueKind, "StringValue"); strictEqual(value.type.kind, "Scalar"); strictEqual(value.type.name, "string"); @@ -65,13 +65,13 @@ describe("string templates", () => { }); it("interpolate another const", async () => { - const value = await compileValueType(`string("one \${a} def")`, `const a = "abc";`); + const value = await compileValue(`string("one \${a} def")`, `const a = "abc";`); strictEqual(value.valueKind, "StringValue"); strictEqual(value.value, "one abc def"); }); it("emit error if string template is not serializable to string", async () => { - const diagnostics = await diagnoseValueType(`string("one \${boolean} def")`); + const diagnostics = await diagnoseValue(`string("one \${boolean} def")`); expectDiagnostics(diagnostics, { code: "non-literal-string-template", message: @@ -80,10 +80,7 @@ describe("string templates", () => { }); it("emit error if string template if interpolating non serializable value", async () => { - const diagnostics = await diagnoseValueType( - `string("one \${a} def")`, - `const a = #{a: "foo"};` - ); + const diagnostics = await diagnoseValue(`string("one \${a} def")`, `const a = #{a: "foo"};`); expectDiagnostics(diagnostics, { code: "non-literal-string-template", message: @@ -115,7 +112,7 @@ describe("validate literal are assignable", () => { describe.each(cases)("%s", (scalarName, values) => { it.each(values)(`%s %s`, async (expected, value, message) => { - const diagnostics = await diagnoseValueType(`a`, `const a:${scalarName} = ${value};`); + const diagnostics = await diagnoseValue(`a`, `const a:${scalarName} = ${value};`); expectDiagnostics(diagnostics, expected === "✔" ? [] : [{ message: message ?? "" }]); }); }); diff --git a/packages/compiler/test/checker/values/utils.ts b/packages/compiler/test/checker/values/utils.ts index 7e383956dc..88eba8c830 100644 --- a/packages/compiler/test/checker/values/utils.ts +++ b/packages/compiler/test/checker/values/utils.ts @@ -23,7 +23,7 @@ export async function diagnoseUsage( return { diagnostics, pos, end }; } -export async function compileAndDiagnoseValueType( +export async function compileAndDiagnoseValue( code: string, other?: string ): Promise<[Value | undefined, readonly Diagnostic[]]> { @@ -49,18 +49,15 @@ export async function compileAndDiagnoseValueType( return [called, diagnostics]; } -export async function compileValueType(code: string, other?: string): Promise { - const [called, diagnostics] = await compileAndDiagnoseValueType(code, other); +export async function compileValue(code: string, other?: string): Promise { + const [called, diagnostics] = await compileAndDiagnoseValue(code, other); expectDiagnosticEmpty(diagnostics); ok(called, "Decorator was not called"); return called; } -export async function diagnoseValueType( - code: string, - other?: string -): Promise { - const [_, diagnostics] = await compileAndDiagnoseValueType(code, other); +export async function diagnoseValue(code: string, other?: string): Promise { + const [_, diagnostics] = await compileAndDiagnoseValue(code, other); return diagnostics; } From 894d08c4737d004f3c86544bcda91fb6c8deb4a8 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 12:00:21 -0700 Subject: [PATCH 109/184] Changelog --- .../feature-object-literals-2024-3-16-11-58-32.md | 7 +++++++ .../changes/feature-object-literals-2024-3-16-12-0-15.md | 7 +++++++ packages/rest/test/routes.test.ts | 9 +++------ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .chronus/changes/feature-object-literals-2024-3-16-11-58-32.md create mode 100644 .chronus/changes/feature-object-literals-2024-3-16-12-0-15.md diff --git a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md new file mode 100644 index 0000000000..2bfb48a17b --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/openapi3" +--- + +Add support for new object and tuple literals as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-12-0-15.md b/.chronus/changes/feature-object-literals-2024-3-16-12-0-15.md new file mode 100644 index 0000000000..83844db17a --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-3-16-12-0-15.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/rest" +--- + +Update types to support new values in TypeSpec \ No newline at end of file diff --git a/packages/rest/test/routes.test.ts b/packages/rest/test/routes.test.ts index 7094b4b448..08819f8392 100644 --- a/packages/rest/test/routes.test.ts +++ b/packages/rest/test/routes.test.ts @@ -421,12 +421,9 @@ describe("rest: routes", () => { } ` ); - strictEqual(diagnostics.length, 1); - strictEqual(diagnostics[0].code, "invalid-argument"); - strictEqual( - diagnostics[0].message, - `Argument '"x"' is not assignable to parameter of type '"/" | ":" | "/:"'` - ); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + }); }); it("skips templated operations", async () => { From 265634efa0450b49eba49d7c6ac947ab84beae50 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Apr 2024 15:50:52 -0700 Subject: [PATCH 110/184] Update .chronus/changes/feature-object-literals-32024-2-15-18-36-3.md Co-authored-by: Brian Terlson --- .chronus/changes/feature-object-literals-32024-2-15-18-36-3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md index b2909027a0..c0edf6219b 100644 --- a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md +++ b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md @@ -5,4 +5,4 @@ packages: - "@typespec/http" --- -Update Flow Template to make sure of new tuple literals +Update Flow Template to make use of the new tuple literals From ac79e22938f7ebd937913d91c0bf4642452ca5b2 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 18 Apr 2024 10:07:49 -0700 Subject: [PATCH 111/184] add notes --- docs/language-basics/values.md | 131 +++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/language-basics/values.md diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md new file mode 100644 index 0000000000..a478f66033 --- /dev/null +++ b/docs/language-basics/values.md @@ -0,0 +1,131 @@ +--- +id: values +title: Values +--- + +# Values + +TypeSpec can define values in addition to types. Values are useful in an API description to define default values for types or provide example values. They are also useful when passing data to decorators, and for template parameters that are ultimately passed to decorators. + +There are three kinds of values: objects, arrays, and scalars. These values can be created with object literals, array literals, and scalar literals and initializers. Additionally, values can result from referencing enum members and union variants. + +## Object values + +Object values use the syntax `#{}` and can define any number of properties. For example: + +```typespec +const point = #{ + x: 0, + y: 0 +} +``` + +The object value's properties must refer to other values. It is an error to reference a type. + +```typespec +const example = #{ + prop1: #{ nested: true }; // ok + prop2: { nested: true }; // error: values can't reference a type + prop3: string; // error: values can't reference a type +} +``` + +## Array values + +Array values use the syntax `#[]` and can define any number of items. For example: + +```typespec +const points = #[ + #{ x: 0, y: 0}, + #{ x: 1, y: 1} +] +``` + +As with object values, array values cannot contain types. + +If an array type defines a minimum and maximum size using the `@minValue` and `@maxValue` decorators, the compiler will validate that the array value has the appropriate number of items. For example: + +```typespec +/** Can have at most 3 tags */ +@maxItems(2) +model Tags is Array; + +const exampleTags1: Tags = #["TypeSpec", "JSON"]; // ok +const exampleTags2: Tags = #["TypeSpec", "JSON", "OpenAPI"]; // error +``` + +## Scalar values + +There are two ways to create scalar values: with a literal syntax like `"string value"`, and with a scalar initializer like `utcDateTime.fromISO("2020-12-01T12:00:00Z")`. + +### Scalar literals + +The literal syntax for strings, numerics, booleans and null can evaluate to either a type or a value depending on the surrounding context of the literal. When the literal is in _type context_ (a model property type, operation return type, alias definition, etc.) the literal becomes a literal type. When the literal is in _value context_ (a default value, property of an object value, const definition, etc.), the literal becomes a value. When the literal is in an _ambiguous context_ (e.g. an argument to a template or decorator that can accept either a type or a value) the literal becomes a value. The `typeof` operator can be used to convert the literal to a type in such ambiguous contexts. + +```typespec +extern dec setNumberValue(target: unknown, color: valueof numeric); +extern dec setNumberType(target: unknown, color: numeric); +extern dec setNumberTypeOrValue(target: unknown, color: numeric | (valueof numeric)); + +@setNumberValue(123) // Passes the scalar value `numeric(123)`. +@setNumberType(123) // Passes the numeric literal type 123. +@setNumberTypeOrValue(123) // passes the scalar value `numeric(123)`. +model A {} +``` + +### Scalar initializers + +Scalar initializers create scalar values by calling an initializer with one or more values. Scalar initializers for types extended from `numeric`, `string`, and `boolean` are called by adding parenthesis after the scalar reference: + +```typespec +const n = int8(100); +const s = string("hello"); +``` + +Any scalar can additionally be declared with named initializers that take one or more value parameters. For example, `utcDateTime` provides a `fromISO` initializer that takes an ISO string. Named scalars can be declared like so: + +```typespec +scalar IPv4 extends string { + init fromInt(uint32); +} + +const ip = IPv4.fromInt(2341230); +``` + +## Enum member & union variant references + +References to enum members and union variants can be either types or values and follow the same rules as scalar literals. When an enum member reference is in _type context_, the reference becomes an enum member type. When in _value context_ or _ambiguous context_ the reference becomes the enum member's value. + +```typespec +extern dec setColorValue(target: unknown, color: valueof string); +extern dec setColorMember(target: unknown, color: Reflection.EnumMember); + +enum Color { + red, + green, + blue, +} + +@setColorValue(Color.red) // same as passing the literal "red" +@setColorMember(Color.red) // passes the enum member Color.red +model A {} +``` + +When a union variant reference is in _type context_, the reference becomes the type of the union variant. When in _value context_ or _ambiguous context_ the reference becomes the value of the union variant. It is an error to refer to a union variant whose type is not a literal type. + +```typespec +extern dec setColorValue(target: unknown, color: valueof string); +extern dec setColorType(target: unknown, color: string); + +union Color { + red: "red", + green: "green", + blue: "blue", + other: string, +} + +@setColorValue(Color.red) // passes the scalar value `string("red")` +@setColorValue(Color.other) // error, trying to pass a type as a value. +@setColorType(Color.red) // passes the string literal type `"red"` +model A {} +``` From dd899602d98573ba30e53a15b16ff62f6c74bed8 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 18 Apr 2024 10:54:45 -0700 Subject: [PATCH 112/184] Update docs/language-basics/values.md Co-authored-by: Timothee Guerin --- docs/language-basics/values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index a478f66033..49170c9f02 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -89,7 +89,7 @@ scalar IPv4 extends string { init fromInt(uint32); } -const ip = IPv4.fromInt(2341230); +const ip = ipv4.fromInt(2341230); ``` ## Enum member & union variant references From c4f0c7fd5952b9060448bd87edc4d2efd5badbb7 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 18 Apr 2024 10:54:51 -0700 Subject: [PATCH 113/184] Update docs/language-basics/values.md Co-authored-by: Timothee Guerin --- docs/language-basics/values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 49170c9f02..bad28e9796 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -85,7 +85,7 @@ const s = string("hello"); Any scalar can additionally be declared with named initializers that take one or more value parameters. For example, `utcDateTime` provides a `fromISO` initializer that takes an ISO string. Named scalars can be declared like so: ```typespec -scalar IPv4 extends string { +scalar ipv4 extends string { init fromInt(uint32); } From c5a2564983d61e3acb25e3cb8267fe611b69930a Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 18 Apr 2024 11:55:05 -0700 Subject: [PATCH 114/184] Update docs/language-basics/values.md Co-authored-by: Timothee Guerin --- docs/language-basics/values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index bad28e9796..7f5edd441e 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -86,7 +86,7 @@ Any scalar can additionally be declared with named initializers that take one or ```typespec scalar ipv4 extends string { - init fromInt(uint32); + init fromInt(value: uint32); } const ip = ipv4.fromInt(2341230); From 1dd06c2ecb3bb2c301fb915135493cb54cf3583b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 18 Apr 2024 15:54:43 -0700 Subject: [PATCH 115/184] abc --- packages/compiler/src/core/checker.ts | 23 +- packages/compiler/src/core/index.ts | 19 ++ .../compiler/src/core/intrinsic-type-data.ts | 210 +++++++++++++++++ packages/compiler/src/core/type-utils.ts | 18 +- packages/compiler/src/lib/decorators.ts | 212 +++--------------- .../compiler/src/lib/intrinsic-decorators.ts | 17 +- packages/compiler/src/server/type-details.ts | 2 +- .../test/decorators/range-limits.test.ts | 4 +- 8 files changed, 305 insertions(+), 200 deletions(-) create mode 100644 packages/compiler/src/core/intrinsic-type-data.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3771cae392..3db922b26d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,14 +1,4 @@ -import { - $docFromComment, - getMaxLength, - getMaxValueAsNumeric, - getMaxValueExclusiveAsNumeric, - getMinLength, - getMinValueAsNumeric, - getMinValueExclusiveAsNumeric, - isArrayModelType, -} from "../lib/decorators.js"; -import { getIndexer } from "../lib/intrinsic-decorators.js"; +import { $docFromComment, getIndexer } from "../lib/intrinsic-decorators.js"; import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; @@ -30,6 +20,14 @@ import { getTypeName, stringTemplateToString, } from "./helpers/index.js"; +import { + getMaxLength, + getMaxValueAsNumeric, + getMaxValueExclusiveAsNumeric, + getMinLength, + getMinValueAsNumeric, + getMinValueExclusiveAsNumeric, +} from "./intrinsic-type-data.js"; import { canNumericConstraintBeJsNumber, legacyMarshallTypeForJS, @@ -44,11 +42,12 @@ import { hasParseError, visitChildren, } from "./parser.js"; -import { Program, ProjectedProgram } from "./program.js"; +import type { Program, ProjectedProgram } from "./program.js"; import { createProjectionMembers } from "./projection-members.js"; import { getFullyQualifiedSymbolName, getParentTemplateNode, + isArrayModelType, isErrorType, isNeverType, isTemplateInstance, diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index bd40432263..cb7063cba8 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -6,6 +6,25 @@ export * from "./diagnostics.js"; export * from "./emitter-utils.js"; export * from "./formatter.js"; export * from "./helpers/index.js"; +export { + getDocData, + getMaxItems, + getMaxItemsAsNumeric, + getMaxLength, + getMaxLengthAsNumeric, + getMaxValue, + getMaxValueAsNumeric, + getMaxValueExclusive, + getMaxValueExclusiveAsNumeric, + getMinItems, + getMinItemsAsNumeric, + getMinLength, + getMinLengthAsNumeric, + getMinValue, + getMinValueAsNumeric, + getMinValueExclusive, + getMinValueExclusiveAsNumeric, +} from "./intrinsic-type-data.js"; export { // eslint-disable-next-line deprecation/deprecation createCadlLibrary, diff --git a/packages/compiler/src/core/intrinsic-type-data.ts b/packages/compiler/src/core/intrinsic-type-data.ts new file mode 100644 index 0000000000..c3ed152b7f --- /dev/null +++ b/packages/compiler/src/core/intrinsic-type-data.ts @@ -0,0 +1,210 @@ +// Contains all intrinsic data setter or getter +// Anything that the TypeSpec check might should be here. + +import type { Type } from "./index.js"; +import type { Numeric } from "./numeric.js"; +import type { Program } from "./program.js"; + +function createStateSymbol(name: string) { + return Symbol.for(`TypeSpec.${name}`); +} + +const stateKeys = { + minValues: createStateSymbol("minValues"), + maxValues: createStateSymbol("maxValues"), + minValueExclusive: createStateSymbol("minValueExclusive"), + maxValueExclusive: createStateSymbol("maxValueExclusive"), + minLength: createStateSymbol("minLengthValues"), + maxLength: createStateSymbol("maxLengthValues"), + minItems: createStateSymbol("minItems"), + maxItems: createStateSymbol("maxItems"), + + docs: createStateSymbol("docs"), + returnDocs: createStateSymbol("returnsDocs"), + errorsDocs: createStateSymbol("errorDocs"), +}; + +// #region @minValue + +export function setMinValue(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.minValues).set(target, value); +} + +export function getMinValueAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.minValues).get(target); +} + +export function getMinValue(program: Program, target: Type): number | undefined { + return getMinValueAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @minValue + +// #region @maxValue + +export function setMaxValue(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.maxValues).set(target, value); +} + +export function getMaxValueAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.maxValues).get(target); +} +export function getMaxValue(program: Program, target: Type): number | undefined { + return getMaxValueAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @maxValue + +// #region @minValueExclusive + +export function setMinValueExclusive(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.minValueExclusive).set(target, value); +} + +export function getMinValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.minValueExclusive).get(target); +} + +export function getMinValueExclusive(program: Program, target: Type): number | undefined { + return getMinValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @minValueExclusive + +// #region @maxValueExclusive +export function setMaxValueExclusive(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.maxValueExclusive).set(target, value); +} + +export function getMaxValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.maxValueExclusive).get(target); +} + +export function getMaxValueExclusive(program: Program, target: Type): number | undefined { + return getMaxValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @maxValueExclusive + +// #region @minLength +export function setMinLength(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.minLength).set(target, value); +} + +/** + * Get the minimum length of a string type as a {@link Numeric} value. + * @param program Current program + * @param target Type with the `@minLength` decorator + */ +export function getMinLengthAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.minLength).get(target); +} + +export function getMinLength(program: Program, target: Type): number | undefined { + return getMinLengthAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @minLength + +// #region @maxLength +export function setMaxLength(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.minLength).set(target, value); +} + +/** + * Get the minimum length of a string type as a {@link Numeric} value. + * @param program Current program + * @param target Type with the `@maxLength` decorator + */ +export function getMaxLengthAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.maxItems).get(target); +} + +export function getMaxLength(program: Program, target: Type): number | undefined { + return getMaxLengthAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @maxLength + +// #region @minItems +export function setMinItems(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.minItems).set(target, value); +} + +export function getMinItemsAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.minItems).get(target); +} + +export function getMinItems(program: Program, target: Type): number | undefined { + return getMinItemsAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @minItems + +// #region @minItems +export function setMaxItems(program: Program, target: Type, value: Numeric): void { + program.stateMap(stateKeys.maxItems).set(target, value); +} + +export function getMaxItemsAsNumeric(program: Program, target: Type): Numeric | undefined { + return program.stateMap(stateKeys.maxItems).get(target); +} + +export function getMaxItems(program: Program, target: Type): number | undefined { + return getMaxItemsAsNumeric(program, target)?.asNumber() ?? undefined; +} +// #endregion @maxItems + +// #region doc + +/** @internal */ +export type DocTarget = "self" | "returns" | "errors"; + +export interface DocData { + /** + * Doc value. + */ + value: string; + + /** + * How was the doc set. + * - `decorator` means the `@doc` decorator was used + * - `comment` means it was set from a `/** comment * /` + */ + source: "decorator" | "comment"; +} + +/** @internal */ +export function setDocData(program: Program, target: Type, key: DocTarget, data: DocData) { + program.stateMap(getDocKey(key)).set(target, data); +} + +function getDocKey(target: DocTarget): symbol { + switch (target) { + case "self": + return stateKeys.docs; + case "returns": + return stateKeys.returnDocs; + case "errors": + return stateKeys.errorsDocs; + } +} + +/** + * @internal + * Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc} + * @param program Program + * @param target Type + * @returns Doc data with source information. + */ +export function getDocDataInternal( + program: Program, + target: Type, + key: DocTarget +): DocData | undefined { + return program.stateMap(getDocKey(key)).get(target); +} + +/** + * Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc} + * @param program Program + * @param target Type + * @returns Doc data with source information. + */ +export function getDocData(program: Program, target: Type): DocData | undefined { + return getDocDataInternal(program, target, "self"); +} +// #endregion doc diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index 39eeb5c5e8..bd2e4ef0bb 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -1,5 +1,6 @@ -import { Program } from "./program.js"; +import type { Program } from "./program.js"; import { + ArrayModelType, Entity, Enum, ErrorType, @@ -49,6 +50,21 @@ export function isValue(entity: Entity): entity is Value { return "valueKind" in entity; } +/** + * @param type Model type + */ +export function isArrayModelType(program: Program, type: Model): type is ArrayModelType { + return Boolean(type.indexer && type.indexer.key.name === "integer"); +} + +/** + * Check if a model is an array type. + * @param type Model type + */ +export function isRecordModelType(program: Program, type: Model): type is ArrayModelType { + return Boolean(type.indexer && type.indexer.key.name === "string"); +} + /** * Lookup and find the node * @param node Node diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 8d6cc940a6..65093cb43d 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -1,4 +1,4 @@ -import { +import type { DeprecatedDecorator, DiscriminatorDecorator, DocDecorator, @@ -48,13 +48,33 @@ import { getDiscriminatedUnion, getTypeName, ignoreDiagnostics, + isArrayModelType, reportDeprecated, validateDecoratorUniqueOnNode, } from "../core/index.js"; +import { + DocData, + getDocDataInternal, + getMaxItemsAsNumeric, + getMaxLengthAsNumeric, + getMaxValueAsNumeric, + getMaxValueExclusiveAsNumeric, + getMinItemsAsNumeric, + getMinLengthAsNumeric, + getMinValueAsNumeric, + getMinValueExclusiveAsNumeric, + setDocData, + setMaxItems, + setMaxLength, + setMaxValue, + setMaxValueExclusive, + setMinLength, + setMinValue, + setMinValueExclusive, +} from "../core/intrinsic-type-data.js"; import { createDiagnostic, reportDiagnostic } from "../core/messages.js"; import { Program, ProjectedProgram } from "../core/program.js"; import { - ArrayModelType, DecoratorContext, Enum, EnumMember, @@ -115,24 +135,6 @@ export function getSummary(program: Program, type: Type): string | undefined { return program.stateMap(summaryKey).get(type); } -const docsKey = createStateSymbol("docs"); -const returnsDocsKey = createStateSymbol("returnsDocs"); -const errorsDocsKey = createStateSymbol("errorDocs"); -type DocTarget = "self" | "returns" | "errors"; - -export interface DocData { - /** - * Doc value. - */ - value: string; - - /** - * How was the doc set. - * - `decorator` means the `@doc` decorator was used - * - `comment` means it was set from a `/** comment * /` - */ - source: "decorator" | "comment"; -} /** * @doc attaches a documentation string. Works great with multi-line string literals. * @@ -154,57 +156,6 @@ export const $doc: DocDecorator = ( setDocData(context.program, target, "self", { value: text, source: "decorator" }); }; -/** - * @internal to be used to set the `@doc` from doc comment. - */ -export const $docFromComment = ( - context: DecoratorContext, - target: Type, - key: DocTarget, - text: string -) => { - setDocData(context.program, target, key, { value: text, source: "comment" }); -}; - -function getDocKey(target: DocTarget): symbol { - switch (target) { - case "self": - return docsKey; - case "returns": - return returnsDocsKey; - case "errors": - return errorsDocsKey; - } -} - -function setDocData(program: Program, target: Type, key: DocTarget, data: DocData) { - program.stateMap(getDocKey(key)).set(target, data); -} - -/** - * Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc} - * @param program Program - * @param target Type - * @returns Doc data with source information. - */ -export function getDocDataInternal( - program: Program, - target: Type, - key: DocTarget -): DocData | undefined { - return program.stateMap(getDocKey(key)).get(target); -} - -/** - * Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc} - * @param program Program - * @param target Type - * @returns Doc data with source information. - */ -export function getDocData(program: Program, target: Type): DocData | undefined { - return getDocDataInternal(program, target, "self"); -} - /** * Get the documentation string for the given type. * @param program Program @@ -358,21 +309,6 @@ function validateTargetingAString( return valid; } -/** - * @param type Model type - */ -export function isArrayModelType(program: Program, type: Model): type is ArrayModelType { - return Boolean(type.indexer && type.indexer.key.name === "integer"); -} - -/** - * Check if a model is an array type. - * @param type Model type - */ -export function isRecordModelType(program: Program, type: Model): type is ArrayModelType { - return Boolean(type.indexer && type.indexer.key.name === "string"); -} - /** * Return the type of the property or the model itself. */ @@ -513,8 +449,6 @@ export function getPatternData(program: Program, target: Type): PatternData | un // -- @minLength decorator --------------------- -const minLengthValuesKey = createStateSymbol("minLengthValues"); - export const $minLength: MinLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, @@ -528,27 +462,11 @@ export const $minLength: MinLengthDecorator = ( ) { return; } - - context.program.stateMap(minLengthValuesKey).set(target, minLength); + setMinLength(context.program, target, minLength); }; -/** - * Get the minimum length of a string type as a {@link Numeric} value. - * @param program Current program - * @param target Type with the `@minLength` decorator - */ -export function getMinLengthAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(minLengthValuesKey).get(target); -} - -export function getMinLength(program: Program, target: Type): number | undefined { - return getMinLengthAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @maxLength decorator --------------------- -const maxLengthValuesKey = createStateSymbol("maxLengthValues"); - export const $maxLength: MaxLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, @@ -563,26 +481,11 @@ export const $maxLength: MaxLengthDecorator = ( return; } - context.program.stateMap(maxLengthValuesKey).set(target, maxLength); + setMaxLength(context.program, target, maxLength); }; -export function getMaxLength(program: Program, target: Type): number | undefined { - return getMaxLengthAsNumeric(program, target)?.asNumber() ?? undefined; -} - -/** - * Get the maximum length of a string type as a {@link Numeric} value. - * @param program Current program - * @param target Type with the `@maxLength` decorator - */ -export function getMaxLengthAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(maxLengthValuesKey).get(target); -} - // -- @minItems decorator --------------------- -const minItemsValuesKey = createStateSymbol("minItems"); - export const $minItems: MinItemsDecorator = ( context: DecoratorContext, target: Type, @@ -605,21 +508,11 @@ export const $minItems: MinItemsDecorator = ( return; } - context.program.stateMap(minItemsValuesKey).set(target, minItems); + setMinLength(context.program, target, minItems); }; -export function getMinItemsAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(minItemsValuesKey).get(target); -} - -export function getMinItems(program: Program, target: Type): number | undefined { - return getMinItemsAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @maxLength decorator --------------------- -const maxItemsValuesKey = createStateSymbol("maxItems"); - export const $maxItems: MaxItemsDecorator = ( context: DecoratorContext, target: Type, @@ -641,21 +534,11 @@ export const $maxItems: MaxItemsDecorator = ( return; } - context.program.stateMap(maxItemsValuesKey).set(target, maxItems); + setMaxItems(context.program, target, maxItems); }; -export function getMaxItemsAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(maxItemsValuesKey).get(target); -} - -export function getMaxItems(program: Program, target: Type): number | undefined { - return getMaxItemsAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @minValue decorator --------------------- -const minValuesKey = createStateSymbol("minValues"); - export const $minValue: MinValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, @@ -679,20 +562,11 @@ export const $minValue: MinValueDecorator = ( ) { return; } - program.stateMap(minValuesKey).set(target, minValue); + setMinValue(program, target, minValue); }; -export function getMinValueAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(minValuesKey).get(target); -} -export function getMinValue(program: Program, target: Type): number | undefined { - return getMinValueAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @maxValue decorator --------------------- -const maxValuesKey = createStateSymbol("maxValues"); - export const $maxValue: MaxValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, @@ -715,20 +589,11 @@ export const $maxValue: MaxValueDecorator = ( ) { return; } - program.stateMap(maxValuesKey).set(target, maxValue); + setMaxValue(program, target, maxValue); }; -export function getMaxValueAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(maxValuesKey).get(target); -} -export function getMaxValue(program: Program, target: Type): number | undefined { - return getMaxValueAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @minValueExclusive decorator --------------------- -const minValueExclusiveKey = createStateSymbol("minValueExclusive"); - export const $minValueExclusive: MinValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, @@ -752,20 +617,11 @@ export const $minValueExclusive: MinValueExclusiveDecorator = ( ) { return; } - program.stateMap(minValueExclusiveKey).set(target, minValueExclusive); + setMinValueExclusive(program, target, minValueExclusive); }; -export function getMinValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(minValueExclusiveKey).get(target); -} -export function getMinValueExclusive(program: Program, target: Type): number | undefined { - return getMinValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @maxValueExclusive decorator --------------------- -const maxValueExclusiveKey = createStateSymbol("maxValueExclusive"); - export const $maxValueExclusive: MaxValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, @@ -788,16 +644,8 @@ export const $maxValueExclusive: MaxValueExclusiveDecorator = ( ) { return; } - program.stateMap(maxValueExclusiveKey).set(target, maxValueExclusive); + setMaxValueExclusive(program, target, maxValueExclusive); }; - -export function getMaxValueExclusiveAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(maxValueExclusiveKey).get(target); -} -export function getMaxValueExclusive(program: Program, target: Type): number | undefined { - return getMaxValueExclusiveAsNumeric(program, target)?.asNumber() ?? undefined; -} - // -- @secret decorator --------------------- const secretTypesKey = createStateSymbol("secretTypes"); diff --git a/packages/compiler/src/lib/intrinsic-decorators.ts b/packages/compiler/src/lib/intrinsic-decorators.ts index 6df1b9904c..ff267c1fe8 100644 --- a/packages/compiler/src/lib/intrinsic-decorators.ts +++ b/packages/compiler/src/lib/intrinsic-decorators.ts @@ -1,5 +1,6 @@ -import { Program } from "../core/program.js"; -import { DecoratorContext, ModelIndexer, Scalar, Type } from "../core/types.js"; +import { DocTarget, setDocData } from "../core/intrinsic-type-data.js"; +import type { Program } from "../core/program.js"; +import type { DecoratorContext, ModelIndexer, Scalar, Type } from "../core/types.js"; export const namespace = "TypeSpec"; @@ -12,3 +13,15 @@ export const $indexer = (context: DecoratorContext, target: Type, key: Scalar, v export function getIndexer(program: Program, target: Type): ModelIndexer | undefined { return program.stateMap(indexTypeKey).get(target); } + +/** + * @internal to be used to set the `@doc` from doc comment. + */ +export const $docFromComment = ( + context: DecoratorContext, + target: Type, + key: DocTarget, + text: string +) => { + setDocData(context.program, target, key, { value: text, source: "comment" }); +}; diff --git a/packages/compiler/src/server/type-details.ts b/packages/compiler/src/server/type-details.ts index 67876a374e..2763bd741d 100644 --- a/packages/compiler/src/server/type-details.ts +++ b/packages/compiler/src/server/type-details.ts @@ -1,6 +1,7 @@ import { compilerAssert, DocContent, + getDocData, Node, Program, Sym, @@ -8,7 +9,6 @@ import { TemplateDeclarationNode, Type, } from "../core/index.js"; -import { getDocData } from "../lib/decorators.js"; import { getSymbolSignature } from "./type-signature.js"; /** diff --git a/packages/compiler/test/decorators/range-limits.test.ts b/packages/compiler/test/decorators/range-limits.test.ts index 64402bdc20..a093c176e4 100644 --- a/packages/compiler/test/decorators/range-limits.test.ts +++ b/packages/compiler/test/decorators/range-limits.test.ts @@ -1,6 +1,5 @@ import { strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Model } from "../../src/core/types.js"; import { getMaxItems, getMaxLength, @@ -8,7 +7,8 @@ import { getMinItems, getMinLength, getMinValue, -} from "../../src/lib/decorators.js"; +} from "../../src/core/intrinsic-type-data.js"; +import { Model } from "../../src/core/types.js"; import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.js"; describe("compiler: range limiting decorators", () => { From fd01267d3ac4c408ab178997bba9b4ae8865e73e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 08:25:55 -0700 Subject: [PATCH 116/184] Validate min/max items for tuples to array --- packages/compiler/src/core/checker.ts | 72 +++++++++++++++---- packages/compiler/src/lib/decorators.ts | 3 +- .../compiler/test/checker/relation.test.ts | 46 ++++++++++-- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3db922b26d..4e777e7426 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -21,9 +21,11 @@ import { stringTemplateToString, } from "./helpers/index.js"; import { + getMaxItems, getMaxLength, getMaxValueAsNumeric, getMaxValueExclusiveAsNumeric, + getMinItems, getMinLength, getMinValueAsNumeric, getMinValueExclusiveAsNumeric, @@ -59,6 +61,7 @@ import { import { AliasStatementNode, ArrayExpressionNode, + ArrayModelType, ArrayValue, AugmentDecoratorStatementNode, BooleanLiteral, @@ -6803,18 +6806,7 @@ export function createChecker(program: Program): Checker { isArrayModelType(program, target) && source.kind === "Tuple" ) { - for (const item of source.values) { - const [related, diagnostics] = isTypeAssignableToInternal( - item, - target.indexer.value!, - diagnosticTarget, - relationCache - ); - if (!related) { - return [Related.false, diagnostics]; - } - } - return [Related.true, []]; + return isTupleAssignableToArray(source, target, diagnosticTarget, relationCache); } else if (target.kind === "Tuple" && source.kind === "Tuple") { return isTupleAssignableToTuple(source, target, diagnosticTarget, relationCache); } else if (target.kind === "Union") { @@ -7176,6 +7168,62 @@ export function createChecker(program: Program): Checker { ); } + function isTupleAssignableToArray( + source: Tuple, + target: ArrayModelType, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Entity, Entity], Related> + ): [Related, readonly Diagnostic[]] { + const minItems = getMinItems(program, target); + const maxItems = getMaxItems(program, target); + if (minItems !== undefined && source.values.length < minItems) { + return [ + Related.false, + [ + createDiagnostic({ + code: "unassignable", + messageId: "withDetails", + format: { + sourceType: getEntityName(source), + targetType: getTypeName(target), + details: `Source has ${source.values.length} element(s) but target requires ${minItems}.`, + }, + target: diagnosticTarget, + }), + ], + ]; + } + if (maxItems !== undefined && source.values.length > maxItems) { + return [ + Related.false, + [ + createDiagnostic({ + code: "unassignable", + messageId: "withDetails", + format: { + sourceType: getEntityName(source), + targetType: getTypeName(target), + details: `Source has ${source.values.length} element(s) but target only allows ${maxItems}.`, + }, + target: diagnosticTarget, + }), + ], + ]; + } + for (const item of source.values) { + const [related, diagnostics] = isTypeAssignableToInternal( + item, + target.indexer.value!, + diagnosticTarget, + relationCache + ); + if (!related) { + return [Related.false, diagnostics]; + } + } + return [Related.true, []]; + } + function isTupleAssignableToTuple( source: Tuple | ArrayValue, target: Tuple, diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 65093cb43d..8056d87744 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -68,6 +68,7 @@ import { setMaxLength, setMaxValue, setMaxValueExclusive, + setMinItems, setMinLength, setMinValue, setMinValueExclusive, @@ -508,7 +509,7 @@ export const $minItems: MinItemsDecorator = ( return; } - setMinLength(context.program, target, minItems); + setMinItems(context.program, target, minItems); }; // -- @maxLength decorator --------------------- diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index ce2ae45560..ae915a2d91 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -965,12 +965,48 @@ describe("compiler: checker: type relations", () => { await expectTypeAssignable({ source: "int32[]", target: "numeric[]" }); }); - it("can assign a tuple of the same type", async () => { - await expectTypeAssignable({ source: "[int32, int32]", target: "int32[]" }); - }); + describe("can assign tuple", () => { + it("of the same type", async () => { + await expectTypeAssignable({ source: "[int32, int32]", target: "int32[]" }); + }); + + it("of subtype", async () => { + await expectTypeAssignable({ source: "[int32, int32, int32]", target: "numeric[]" }); + }); - it("can assign a tuple of subtype", async () => { - await expectTypeAssignable({ source: "[int32, int32, int32]", target: "numeric[]" }); + it("validate minItems", async () => { + await expectTypeNotAssignable( + { + source: `["one", string]`, + target: "Tags", + commonCode: `@minItems(3) model Tags is string[];`, + }, + { + code: "unassignable", + message: [ + `Type '["one", string]' is not assignable to type 'Tags'`, + ` Source has 2 element(s) but target requires 3.`, + ].join("\n"), + } + ); + }); + + it("validate maxItems", async () => { + await expectTypeNotAssignable( + { + source: `["one", string, "three", "four"]`, + target: "Tags", + commonCode: `@maxItems(3) model Tags is string[];`, + }, + { + code: "unassignable", + message: [ + `Type '["one", string, "three", "four"]' is not assignable to type 'Tags'`, + ` Source has 4 element(s) but target only allows 3.`, + ].join("\n"), + } + ); + }); }); it("emit diagnostic assigning other type", async () => { From 8a1f95c364daeda0a505db6bcc49a158825aeaea Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 09:04:55 -0700 Subject: [PATCH 117/184] fix --- packages/compiler/src/core/checker.ts | 2 +- packages/compiler/src/core/index.ts | 2 +- .../core/{intrinsic-type-data.ts => intrinsic-type-state.ts} | 4 ++-- packages/compiler/src/lib/decorators.ts | 2 +- packages/compiler/src/lib/intrinsic-decorators.ts | 2 +- packages/compiler/test/decorators/range-limits.test.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename packages/compiler/src/core/{intrinsic-type-data.ts => intrinsic-type-state.ts} (98%) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 4e777e7426..7bc4c69390 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -29,7 +29,7 @@ import { getMinLength, getMinValueAsNumeric, getMinValueExclusiveAsNumeric, -} from "./intrinsic-type-data.js"; +} from "./intrinsic-type-state.js"; import { canNumericConstraintBeJsNumber, legacyMarshallTypeForJS, diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index cb7063cba8..b210b09037 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -24,7 +24,7 @@ export { getMinValueAsNumeric, getMinValueExclusive, getMinValueExclusiveAsNumeric, -} from "./intrinsic-type-data.js"; +} from "./intrinsic-type-state.js"; export { // eslint-disable-next-line deprecation/deprecation createCadlLibrary, diff --git a/packages/compiler/src/core/intrinsic-type-data.ts b/packages/compiler/src/core/intrinsic-type-state.ts similarity index 98% rename from packages/compiler/src/core/intrinsic-type-data.ts rename to packages/compiler/src/core/intrinsic-type-state.ts index c3ed152b7f..9d63a9b47a 100644 --- a/packages/compiler/src/core/intrinsic-type-data.ts +++ b/packages/compiler/src/core/intrinsic-type-state.ts @@ -103,7 +103,7 @@ export function getMinLength(program: Program, target: Type): number | undefined // #region @maxLength export function setMaxLength(program: Program, target: Type, value: Numeric): void { - program.stateMap(stateKeys.minLength).set(target, value); + program.stateMap(stateKeys.maxLength).set(target, value); } /** @@ -112,7 +112,7 @@ export function setMaxLength(program: Program, target: Type, value: Numeric): vo * @param target Type with the `@maxLength` decorator */ export function getMaxLengthAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(stateKeys.maxItems).get(target); + return program.stateMap(stateKeys.maxLength).get(target); } export function getMaxLength(program: Program, target: Type): number | undefined { diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 8056d87744..e7d42e421d 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -72,7 +72,7 @@ import { setMinLength, setMinValue, setMinValueExclusive, -} from "../core/intrinsic-type-data.js"; +} from "../core/intrinsic-type-state.js"; import { createDiagnostic, reportDiagnostic } from "../core/messages.js"; import { Program, ProjectedProgram } from "../core/program.js"; import { diff --git a/packages/compiler/src/lib/intrinsic-decorators.ts b/packages/compiler/src/lib/intrinsic-decorators.ts index ff267c1fe8..688e783cf2 100644 --- a/packages/compiler/src/lib/intrinsic-decorators.ts +++ b/packages/compiler/src/lib/intrinsic-decorators.ts @@ -1,4 +1,4 @@ -import { DocTarget, setDocData } from "../core/intrinsic-type-data.js"; +import { DocTarget, setDocData } from "../core/intrinsic-type-state.js"; import type { Program } from "../core/program.js"; import type { DecoratorContext, ModelIndexer, Scalar, Type } from "../core/types.js"; diff --git a/packages/compiler/test/decorators/range-limits.test.ts b/packages/compiler/test/decorators/range-limits.test.ts index a093c176e4..1220135b73 100644 --- a/packages/compiler/test/decorators/range-limits.test.ts +++ b/packages/compiler/test/decorators/range-limits.test.ts @@ -7,7 +7,7 @@ import { getMinItems, getMinLength, getMinValue, -} from "../../src/core/intrinsic-type-data.js"; +} from "../../src/core/intrinsic-type-state.js"; import { Model } from "../../src/core/types.js"; import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.js"; From 8438d6c993ddb1e8e8a580762c3832d08f981aa6 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 10:12:56 -0700 Subject: [PATCH 118/184] remove outdated doc --- docs/language-basics/type-and-values.md | 51 ------------------------- 1 file changed, 51 deletions(-) delete mode 100644 docs/language-basics/type-and-values.md diff --git a/docs/language-basics/type-and-values.md b/docs/language-basics/type-and-values.md deleted file mode 100644 index e14c036858..0000000000 --- a/docs/language-basics/type-and-values.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: "type-and-values" -title: "Type and Values" ---- - -# Type and Values in TypeSpec - -TypeSpec has the concept of Types and Values, entities can be either a Type, a Value or both depending on the context. - -| Entity Name | Type | Value | -| -------------------- | ---- | ----- | -| `Namespace` | ✅ | | -| `Model` | ✅ | | -| `ModelProperty` | ✅ | | -| `Union` | ✅ | | -| `UnionVariant` | ✅ | | -| `Interface` | ✅ | | -| `Operation` | ✅ | | -| `Scalar` | ✅ | | -| `Tuple` | ✅ | | -| `Enum` | ✅ | | -| `EnumMember` | ✅ | ✅ | -| `StringLiteral` | ✅ | ✅ | -| `NumberLiteral` | ✅ | ✅ | -| `BooleanLiteral` | ✅ | ✅ | -| `ObjectLiteral` | | ✅ | -| `TupleLiteral` | | ✅ | -| ---- _intrinsic_ --- | --- | --- | -| `null` | ✅ | ✅ | -| `unknown` | ✅ | | - -## Contexts - -There is 3 context that can exists in TypeSpec: - -- **Type only** This is when an expression can only be a Type. - - Model property type - - Array element type - - Tuple values - - Operation parameters - - Operation return type - - Union variant type with some exceptions when used as a decorator or template parameter constraint. -- **Value only** This is when an expression can only be a Value. - - Default values -- **Type and Value Constaints** This is when an expression can be a type or a `valueof` - - Decorator parameters - - Template parameters -- **Type and Value** This is when an expression can be a type or a value. - - Aliases - - Decorator arguments - - Template arguments From 25b6b9e29ba737cb219bf2aa45f6d9b9f9aa8c88 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 14:23:41 -0700 Subject: [PATCH 119/184] Add parse support for typeof --- grammars/typespec.json | 79 ++++++++++++++++--- packages/compiler/src/core/parser.ts | 16 ++++ packages/compiler/src/core/scanner.ts | 5 +- packages/compiler/src/core/types.ts | 7 ++ .../compiler/src/formatter/print/printer.ts | 11 +++ packages/compiler/src/server/tmlanguage.ts | 43 +++++++++- packages/compiler/test/parser.test.ts | 12 ++- .../compiler/test/server/colorization.test.ts | 18 +++++ packages/spec/src/spec.emu.html | 7 +- 9 files changed, 177 insertions(+), 21 deletions(-) diff --git a/grammars/typespec.json b/grammars/typespec.json index 66e881ff6d..a913cb0379 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -11,24 +11,39 @@ } ], "repository": { + "alias-id": { + "name": "meta.alias-id.typespec", + "begin": "(=)\\s*", + "beginCaptures": { + "1": { + "name": "keyword.operator.assignment.tsp" + } + }, + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "patterns": [ + { + "include": "#expression" + } + ] + }, "alias-statement": { "name": "meta.alias-statement.typespec", - "begin": "\\b(alias)\\b", + "begin": "\\b(alias)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\s*", "beginCaptures": { "1": { "name": "keyword.other.tsp" + }, + "2": { + "name": "entity.name.type.tsp" } }, "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { - "include": "#type-parameters" - }, - { - "include": "#operator-assignment" + "include": "#alias-id" }, { - "include": "#expression" + "include": "#type-parameters" } ] }, @@ -381,6 +396,9 @@ { "include": "#valueof" }, + { + "include": "#typeof" + }, { "include": "#type-arguments" }, @@ -1369,6 +1387,35 @@ } ] }, + "type-argument": { + "name": "meta.type-argument.typespec", + "begin": "(?:(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\s*(=))", + "beginCaptures": { + "1": { + "name": "entity.name.type.tsp" + }, + "2": { + "name": "keyword.operator.assignment.tsp" + } + }, + "end": "=", + "endCaptures": { + "0": { + "name": "keyword.operator.assignment.tsp" + } + }, + "patterns": [ + { + "include": "#token" + }, + { + "include": "#expression" + }, + { + "include": "#punctuation-comma" + } + ] + }, "type-arguments": { "name": "meta.type-arguments.typespec", "begin": "<", @@ -1385,10 +1432,7 @@ }, "patterns": [ { - "include": "#identifier-expression" - }, - { - "include": "#operator-assignment" + "include": "#type-argument" }, { "include": "#expression" @@ -1472,6 +1516,21 @@ } ] }, + "typeof": { + "name": "meta.typeof.typespec", + "begin": "\\b(typeof)", + "beginCaptures": { + "1": { + "name": "keyword.other.tsp" + } + }, + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "patterns": [ + { + "include": "#expression" + } + ] + }, "union-body": { "name": "meta.union-body.typespec", "begin": "\\{", diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 96eb5a16f4..3eda268a3b 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -110,6 +110,7 @@ import { TextRange, TupleExpressionNode, TupleLiteralNode, + TypeOfExpressionNode, TypeReferenceNode, TypeSpecScriptNode, UnionStatementNode, @@ -1296,6 +1297,17 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ...finishNode(pos), }; } + function parseTypeOfExpression(): TypeOfExpressionNode { + const pos = tokenPos(); + parseExpected(Token.TypeOfKeyword); + const target = parseExpression(); + + return { + kind: SyntaxKind.TypeOfExpression, + target, + ...finishNode(pos), + }; + } function parseReferenceExpression( message?: keyof CompilerDiagnostics["token-expected"] @@ -1559,6 +1571,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa switch (token()) { case Token.ValueOfKeyword: return parseValueOfExpression(); + case Token.TypeOfKeyword: + return parseTypeOfExpression(); case Token.Identifier: return parseCallOrReferenceExpression(); case Token.StringLiteral: @@ -3491,6 +3505,8 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined return visitNode(cb, node.target) || visitEach(cb, node.arguments); case SyntaxKind.ValueOfExpression: return visitNode(cb, node.target); + case SyntaxKind.TypeOfExpression: + return visitNode(cb, node.target); case SyntaxKind.TupleExpression: return visitEach(cb, node.values); case SyntaxKind.UnionExpression: diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 97924f89d7..74fd4d5528 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -124,7 +124,6 @@ export enum Token { IfKeyword, DecKeyword, FnKeyword, - ValueOfKeyword, ConstKeyword, InitKeyword, // Add new statement keyword above @@ -151,6 +150,8 @@ export enum Token { VoidKeyword, NeverKeyword, UnknownKeyword, + ValueOfKeyword, + TypeOfKeyword, // Add new non-statement keyword above /** @internal */ __EndKeyword, @@ -249,6 +250,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.DecKeyword, "'dec'"], [Token.FnKeyword, "'fn'"], [Token.ValueOfKeyword, "'valueof'"], + [Token.TypeOfKeyword, "'typeof'"], [Token.ConstKeyword, "'const'"], [Token.InitKeyword, "'init'"], [Token.ExtendsKeyword, "'extends'"], @@ -281,6 +283,7 @@ export const Keywords: ReadonlyMap = new Map([ ["dec", Token.DecKeyword], ["fn", Token.FnKeyword], ["valueof", Token.ValueOfKeyword], + ["typeof", Token.TypeOfKeyword], ["const", Token.ConstKeyword], ["init", Token.InitKeyword], ["true", Token.TrueKeyword], diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 68f22a1703..5cd52d5442 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -932,6 +932,7 @@ export enum SyntaxKind { Return, JsNamespaceDeclaration, TemplateArgument, + TypeOfExpression, ObjectLiteral, ObjectLiteralProperty, ObjectLiteralSpreadProperty, @@ -1206,6 +1207,7 @@ export type Expression = | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode + | TypeOfExpressionNode | CallExpressionNode | StringLiteralNode | NumericLiteralNode @@ -1529,6 +1531,11 @@ export interface ValueOfExpressionNode extends BaseNode { readonly target: Expression; } +export interface TypeOfExpressionNode extends BaseNode { + readonly kind: SyntaxKind.TypeOfExpression; + readonly target: Expression; +} + export interface TypeReferenceNode extends BaseNode { readonly kind: SyntaxKind.TypeReference; readonly target: MemberExpressionNode | IdentifierNode; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 0a233c0822..57dd9645b5 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -72,6 +72,7 @@ import { TextRange, TupleExpressionNode, TupleLiteralNode, + TypeOfExpressionNode, TypeReferenceNode, TypeSpecScriptNode, UnionExpressionNode, @@ -224,6 +225,8 @@ export function printNode( return printTemplateArgument(path as AstPath, options, print); case SyntaxKind.ValueOfExpression: return printValueOfExpression(path as AstPath, options, print); + case SyntaxKind.TypeOfExpression: + return printTypeOfExpression(path as AstPath, options, print); case SyntaxKind.TemplateParameterDeclaration: return printTemplateParameterDeclaration( path as AstPath, @@ -1524,6 +1527,14 @@ export function printValueOfExpression( const type = path.call(print, "target"); return ["valueof ", type]; } +export function printTypeOfExpression( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint +): Doc { + const type = path.call(print, "target"); + return ["typeof ", type]; +} function printTemplateParameterDeclaration( path: AstPath, diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index d341df58ef..0ad09f21f3 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -322,7 +322,31 @@ const valueOfExpression: BeginEndRule = { end: `(?=>)|${universalEnd}`, patterns: [expression], }; +const typeOfExpression: BeginEndRule = { + key: "typeof", + scope: meta, + begin: `\\b(typeof)`, + beginCaptures: { + "1": { scope: "keyword.other.tsp" }, + }, + end: `(?=>)|${universalEnd}`, + patterns: [expression], +}; +const typeArgument: BeginEndRule = { + key: "type-argument", + scope: meta, + begin: `(?:(${identifier})\\s*(=))`, + beginCaptures: { + "1": { scope: "entity.name.type.tsp" }, + "2": { scope: "keyword.operator.assignment.tsp" }, + }, + end: `=`, + endCaptures: { + "0": { scope: "keyword.operator.assignment.tsp" }, + }, + patterns: [token, expression, punctuationComma], +}; const typeArguments: BeginEndRule = { key: "type-arguments", scope: meta, @@ -334,7 +358,7 @@ const typeArguments: BeginEndRule = { endCaptures: { "0": { scope: "punctuation.definition.typeparameters.end.tsp" }, }, - patterns: [identifierExpression, operatorAssignment, expression, punctuationComma], + patterns: [typeArgument, expression, punctuationComma], }; const typeParameterConstraint: BeginEndRule = { @@ -679,15 +703,27 @@ const unionStatement: BeginEndRule = { patterns: [token, unionBody], }; +const aliasAssignment: BeginEndRule = { + key: "alias-id", + scope: meta, + begin: `(=)\\s*`, + beginCaptures: { + "1": { scope: "keyword.operator.assignment.tsp" }, + }, + end: universalEnd, + patterns: [expression], +}; + const aliasStatement: BeginEndRule = { key: "alias-statement", scope: meta, - begin: "\\b(alias)\\b", + begin: `\\b(alias)\\b\\s+(${identifier})\\s*`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, + "2": { scope: "entity.name.type.tsp" }, }, end: universalEnd, - patterns: [typeParameters, operatorAssignment, expression], + patterns: [aliasAssignment, typeParameters], }; const constStatement: BeginEndRule = { @@ -1017,6 +1053,7 @@ expression.patterns = [ directive, parenthesizedExpression, valueOfExpression, + typeOfExpression, typeArguments, objectLiteral, tupleLiteral, diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 9a2c57dbab..eefc1b16b2 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -271,13 +271,17 @@ describe("compiler: parser", () => { describe("valueof expressions", () => { parseEach([ - "alias A = valueof string;", - "alias A = valueof int32;", - "alias A = valueof {a: string, b: int32};", - "alias A = valueof int8[];", + "model Foo {}", + "model Foo {}", + "model Foo {}", + "model Foo {}", ]); }); + describe("typeof expressions", () => { + parseEach([`const a: typeof #{name: "abc"} = 123;`, `alias A = Foo;`]); + }); + describe("template instantiations", () => { parseEach(["model A { x: Foo; }", "model B { x: Foo[]; }"]); }); diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 93acfc85fa..3c8d391aa2 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -51,6 +51,7 @@ const Token = { to: createToken("to", "keyword.other.tsp"), from: createToken("from", "keyword.other.tsp"), valueof: createToken("valueof", "keyword.other.tsp"), + typeof: createToken("typeof", "keyword.other.tsp"), const: createToken("const", "keyword.other.tsp"), other: (text: string) => createToken(text, "keyword.other.tsp"), }, @@ -326,6 +327,23 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); + describe("typeof", () => { + it("simple typeof", async () => { + const tokens = await tokenize(`alias B = Foo;`); + deepStrictEqual(tokens, [ + Token.keywords.alias, + Token.identifiers.type("B"), + Token.operators.assignment, + Token.identifiers.type("Foo"), + Token.punctuation.typeParameters.begin, + Token.keywords.typeof, + Token.literals.stringQuoted("abc"), + Token.punctuation.typeParameters.end, + Token.punctuation.semicolon, + ]); + }); + }); + describe("decorators", () => { it("simple parameterless decorator", async () => { const tokens = await tokenize("@foo"); diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 61d380be36..ea1b1ffc4e 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -477,12 +477,13 @@

Syntactic Grammar

`|`? UnionExpressionOrHigher `|` IntersectionExpressionOrHigher IntersectionExpressionOrHigher : - ValueOfExpressionOrHigher - `&`? IntersectionExpressionOrHigher `&` ValueOfExpressionOrHigher + ValueOfOrTypeOfExpressionOrHigher + `&`? IntersectionExpressionOrHigher `&` ValueOfOrTypeOfExpressionOrHigher -ValueOfExpressionOrHigher : +ValueOfOrTypeOfExpressionOrHigher : ArrayExpressionOrHigher `valueof` ArrayExpressionOrHigher + `typeof` ArrayExpressionOrHigher ArrayExpressionOrHigher : PrimaryExpression From 7afcec15ac5075469a731e843b4b22a29b2a1381 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 19:20:09 -0700 Subject: [PATCH 120/184] Limit typeof syntax and organize valueof --- packages/compiler/src/core/messages.ts | 1 + packages/compiler/src/core/parser.ts | 72 +++++++++++++++++-- .../compiler/test/formatter/formatter.test.ts | 19 ++--- packages/compiler/test/parser.test.ts | 5 +- packages/compiler/test/scanner.test.ts | 2 + packages/spec/src/spec.emu.html | 26 ++++--- 6 files changed, 94 insertions(+), 31 deletions(-) diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index d5757c262c..b2ae93c94f 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -144,6 +144,7 @@ const diagnostics = { statement: "Statement expected.", property: "Property expected.", enumMember: "Enum member expected.", + typeofTarget: "Typeof expect a literal or value reference.", }, }, "trailing-token": { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 3eda268a3b..0defa8948e 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -942,9 +942,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseTemplateParameter(): TemplateParameterDeclarationNode { const pos = tokenPos(); const id = parseIdentifier(); - let constraint: Expression | undefined; + let constraint: Expression | ValueOfExpressionNode | undefined; if (parseOptional(Token.ExtendsKeyword)) { - constraint = parseExpression(); + constraint = parseMixedConstraint(); } let def: Expression | undefined; if (parseOptional(Token.Equals)) { @@ -959,6 +959,40 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseValueOfExpressionOrIntersectionOrHigher() { + if (token() === Token.ValueOfKeyword) { + return parseValueOfExpression(); + } else if (parseOptional(Token.OpenParen)) { + const expr = parseMixedConstraint(); + parseExpected(Token.CloseParen); + return expr; + } + + return parseIntersectionExpressionOrHigher(); + } + + function parseMixedConstraint(): Expression | ValueOfExpressionNode { + const pos = tokenPos(); + parseOptional(Token.Bar); + const node: Expression = parseValueOfExpressionOrIntersectionOrHigher(); + + if (token() !== Token.Bar) { + return node; + } + + const options = [node]; + while (parseOptional(Token.Bar)) { + const expr = parseValueOfExpressionOrIntersectionOrHigher(); + options.push(expr); + } + + return { + kind: SyntaxKind.UnionExpression, + options, + ...finishNode(pos), + }; + } + function parseModelPropertyOrSpread(pos: number, decorators: DecoratorExpressionNode[]) { return token() === Token.Ellipsis ? parseModelSpreadProperty(pos, decorators) @@ -1297,10 +1331,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ...finishNode(pos), }; } + function parseTypeOfExpression(): TypeOfExpressionNode { const pos = tokenPos(); parseExpected(Token.TypeOfKeyword); - const target = parseExpression(); + const target = parseTypeOfTarget(); return { kind: SyntaxKind.TypeOfExpression, @@ -1309,6 +1344,33 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseTypeOfTarget(): Expression { + while (true) { + switch (token()) { + case Token.TypeOfKeyword: + return parseTypeOfExpression(); + case Token.Identifier: + return parseCallOrReferenceExpression(); + case Token.StringLiteral: + return parseStringLiteral(); + case Token.StringTemplateHead: + return parseStringTemplateExpression(); + case Token.TrueKeyword: + case Token.FalseKeyword: + return parseBooleanLiteral(); + case Token.NumericLiteral: + return parseNumericLiteral(); + case Token.OpenParen: + parseExpected(Token.OpenParen); + const target = parseTypeOfTarget(); + parseExpected(Token.CloseParen); + return target; + default: + return parseReferenceExpression("typeofTarget"); + } + } + } + function parseReferenceExpression( message?: keyof CompilerDiagnostics["token-expected"] ): TypeReferenceNode { @@ -1569,8 +1631,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parsePrimaryExpression(): Expression { while (true) { switch (token()) { - case Token.ValueOfKeyword: - return parseValueOfExpression(); case Token.TypeOfKeyword: return parseTypeOfExpression(); case Token.Identifier: @@ -2002,7 +2062,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const optional = parseOptional(Token.Question); let type; if (parseOptional(Token.Colon)) { - type = parseExpression(); + type = parseMixedConstraint(); } return { kind: SyntaxKind.FunctionParameter, diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index 53b6321bbc..43b03224f8 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -2529,10 +2529,10 @@ model Foo { it("format simple valueof", async () => { await assertFormat({ code: ` -alias A = valueof string; +model Foo{} `, expected: ` -alias A = valueof string; +model Foo {} `, }); }); @@ -2540,21 +2540,10 @@ alias A = valueof string; it("keeps parentheses around valueof inside a union", async () => { await assertFormat({ code: ` -alias A = (valueof string) | Model; +model Foo{} `, expected: ` -alias A = (valueof string) | Model; -`, - }); - }); - - it("keeps parentheses around valueof inside a array expression", async () => { - await assertFormat({ - code: ` -alias A = (valueof string)[]; -`, - expected: ` -alias A = (valueof string)[]; +model Foo {} `, }); }); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index eefc1b16b2..cd74413334 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -279,7 +279,10 @@ describe("compiler: parser", () => { }); describe("typeof expressions", () => { - parseEach([`const a: typeof #{name: "abc"} = 123;`, `alias A = Foo;`]); + parseEach([`const a: typeof "123" = 123;`, `alias A = Foo;`]); + parseErrorEach([ + [`alias A = typeof #{}`, [{ message: "Typeof expect a literal or value reference." }]], + ]); }); describe("template instantiations", () => { diff --git a/packages/compiler/test/scanner.test.ts b/packages/compiler/test/scanner.test.ts index fd4ace1ccf..a1715f2ada 100644 --- a/packages/compiler/test/scanner.test.ts +++ b/packages/compiler/test/scanner.test.ts @@ -394,6 +394,8 @@ describe("compiler: scanner", () => { Token.NeverKeyword, Token.UnknownKeyword, Token.ExternKeyword, + Token.ValueOfKeyword, + Token.TypeOfKeyword, ]; let minKeywordLengthFound = Number.MAX_SAFE_INTEGER; let maxKeywordLengthFound = Number.MIN_SAFE_INTEGER; diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index ea1b1ffc4e..cdaadacbb2 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -444,8 +444,12 @@

Syntactic Grammar

Identifier TemplateParameterConstraint? TemplateParameterDefault? TemplateParameterConstraint : - `extends` Expression - + `extends` MixedConstraint + +MixedConstraint : + Expression + ValueOfExpression + TemplateParameterDefault : `=` Expression @@ -477,19 +481,23 @@

Syntactic Grammar

`|`? UnionExpressionOrHigher `|` IntersectionExpressionOrHigher IntersectionExpressionOrHigher : - ValueOfOrTypeOfExpressionOrHigher - `&`? IntersectionExpressionOrHigher `&` ValueOfOrTypeOfExpressionOrHigher +ArrayExpressionOrHigher + `&`? IntersectionExpressionOrHigher `&` ArrayExpressionOrHigher + +ValueOfExpression : + `valueof` Expression -ValueOfOrTypeOfExpressionOrHigher : - ArrayExpressionOrHigher - `valueof` ArrayExpressionOrHigher - `typeof` ArrayExpressionOrHigher +TypeOfExpression + `typeof` Literal + `typeof` ReferenceExpression + `typeof` ParenthesizedExpression ArrayExpressionOrHigher : PrimaryExpression ArrayExpressionOrHigher `[` `]` PrimaryExpression : + TypeOfExpression Literal CallOrReferenceExpression ParenthesizedExpression @@ -589,7 +597,7 @@

Syntactic Grammar

FunctionModifiers? `fn` `(` FunctionParameterList? `)` TypeAnnotation? TypeAnnotation: - `:` Expression + `:` MixedConstraint FunctionModifiers: `extern` From 53c8a74a9ca5fffbe9b60cf3796b620f1853bc50 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 19:38:39 -0700 Subject: [PATCH 121/184] Rename --- packages/compiler/src/core/checker.ts | 38 +++++++++---------- .../src/core/helpers/type-name-utils.ts | 2 +- packages/compiler/src/core/type-utils.ts | 2 +- packages/compiler/src/core/types.ts | 26 ++++++------- .../compiler/src/server/type-signature.ts | 4 +- .../decorators-signatures.ts | 8 ++-- .../tspd/src/ref-doc/utils/type-signature.ts | 4 +- 7 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 7bc4c69390..3fc561b914 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -107,6 +107,7 @@ import { MemberExpressionNode, MemberNode, MemberType, + MixedConstraint, Model, ModelExpressionNode, ModelIndexer, @@ -129,7 +130,6 @@ import { ObjectValuePropertyDescriptor, Operation, OperationStatementNode, - ParamConstraintUnion, Projection, ProjectionArithmeticExpressionNode, ProjectionBlockExpressionNode, @@ -196,8 +196,8 @@ import { UnionVariantNode, UnknownType, Value, + ValueConstraint, ValueOfExpressionNode, - ValueType, VoidType, } from "./types.js"; @@ -879,7 +879,7 @@ export function createChecker(program: Program): Checker { interface CheckConstraint { kind: "argument" | "assignment"; - constraint: Type | ValueType | ParamConstraintUnion; + constraint: Type | ValueConstraint | MixedConstraint; } interface CheckValueConstraint { kind: "argument" | "assignment"; @@ -919,7 +919,7 @@ export function createChecker(program: Program): Checker { return { kind: constraint.kind, type: constraint.constraint.target }; } else { const valueOfOptions = constraint.constraint.options - .filter((x): x is ValueType => x.kind === "Value") + .filter((x): x is ValueConstraint => x.kind === "Value") .map((x) => x.target); return { kind: constraint.kind, type: createUnion(valueOfOptions) }; } @@ -1430,7 +1430,7 @@ export function createChecker(program: Program): Checker { // TODO-TIM check if we expose this below commit( param, - param.constraint?.kind === "Value" || param.constraint?.kind === "ParamConstraintUnion" + param.constraint?.kind === "Value" || param.constraint?.kind === "MixedConstraint" ? unknownType : param.constraint ?? unknownType ); @@ -1450,7 +1450,7 @@ export function createChecker(program: Program): Checker { if (isErrorType(type) || !checkTypeAssignable(type, constraint, argNode)) { // TODO-TIM check if we expose this below const effectiveType = - param.constraint?.kind === "Value" || param.constraint.kind === "ParamConstraintUnion" + param.constraint?.kind === "Value" || param.constraint.kind === "MixedConstraint" ? unknownType : param.constraint; @@ -1748,14 +1748,14 @@ export function createChecker(program: Program): Checker { function checkUnionExpressionAsParamConstraint( node: UnionExpressionNode, mapper: TypeMapper | undefined - ): Union | ParamConstraintUnion { + ): Union | MixedConstraint { const hasValueOf = node.options.some((x) => x.kind === SyntaxKind.ValueOfExpression); if (!hasValueOf) { return checkUnionExpression(node, mapper); } return { - kind: "ParamConstraintUnion", + kind: "MixedConstraint", node, options: node.options.map((x) => getTypeOrValueOfTypeForNode(x, mapper)), }; @@ -1804,7 +1804,7 @@ export function createChecker(program: Program): Checker { function checkValueOfExpression( node: ValueOfExpressionNode, mapper: TypeMapper | undefined - ): ValueType { + ): ValueConstraint { const target = getTypeForNode(node.target, mapper); return { @@ -1989,7 +1989,7 @@ export function createChecker(program: Program): Checker { return parameterType; } - function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): Type | ValueType { + function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): Type | ValueConstraint { switch (node.kind) { case SyntaxKind.ValueOfExpression: return checkValueOfExpression(node, mapper); @@ -2001,7 +2001,7 @@ export function createChecker(program: Program): Checker { function getParamConstraintEntityForNode( node: Node, mapper?: TypeMapper - ): Type | ParamConstraintUnion | ValueType { + ): Type | MixedConstraint | ValueConstraint { switch (node.kind) { case SyntaxKind.UnionExpression: return checkUnionExpressionAsParamConstraint(node, mapper); @@ -3824,7 +3824,7 @@ export function createChecker(program: Program): Checker { for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { const restType = - parameter.type.kind === "ParamConstraintUnion" || parameter.type.kind === "Value" // TODO: change if we change this to not be a FunctionParameter + parameter.type.kind === "MixedConstraint" || parameter.type.kind === "Value" // TODO: change if we change this to not be a FunctionParameter ? undefined : getIndexType(parameter.type); if (restType) { @@ -4756,7 +4756,7 @@ export function createChecker(program: Program): Checker { const jsMarshalling = resolveDecoratorArgMarshalling(declaration); function resolveArg( argNode: Expression, - perParamType: Type | ValueType | ParamConstraintUnion + perParamType: Type | ValueConstraint | MixedConstraint ): DecoratorArgument | undefined { const arg = getTypeOrValueForNode(argNode, mapper, { kind: "argument", @@ -4786,7 +4786,7 @@ export function createChecker(program: Program): Checker { for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { const restType = - parameter.type.kind === "ParamConstraintUnion" + parameter.type.kind === "MixedConstraint" ? undefined : getIndexType( parameter.type.kind === "Value" ? parameter.type.target : parameter.type @@ -6728,7 +6728,7 @@ export function createChecker(program: Program): Checker { if (isValue(target)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - if (target.kind === "ParamConstraintUnion") { + if (target.kind === "MixedConstraint") { return isAssignableToParameterConstraintUnion( source, target, @@ -6737,7 +6737,7 @@ export function createChecker(program: Program): Checker { ); } - if (isValue(source) || source.kind === "Value" || source.kind === "ParamConstraintUnion") { + if (isValue(source) || source.kind === "Value" || source.kind === "MixedConstraint") { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } const isSimpleTypeRelated = isSimpleTypeAssignableTo(source, target); @@ -6820,7 +6820,7 @@ export function createChecker(program: Program): Checker { function isAssignableToValueType( source: Entity, - target: ValueType, + target: ValueConstraint, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { @@ -6843,11 +6843,11 @@ export function createChecker(program: Program): Checker { function isAssignableToParameterConstraintUnion( source: Entity, - target: ParamConstraintUnion, + target: MixedConstraint, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if ("kind" in source && source.kind === "ParamConstraintUnion") { + if ("kind" in source && source.kind === "MixedConstraint") { for (const option of source.options) { const [variantAssignable] = isAssignableToParameterConstraintUnion( option, diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 8c5aca01bc..8ddaeaa645 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -89,7 +89,7 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string switch (entity.kind) { case "Value": return `valueof ${getTypeName(entity.target, options)}`; - case "ParamConstraintUnion": + case "MixedConstraint": return entity.options.map((x) => getEntityName(x, options)).join(" | "); default: return getTypeName(entity, options); diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index bd2e4ef0bb..9eb3537c3b 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -44,7 +44,7 @@ export function isNullType(type: Entity): type is NullType { } export function isType(entity: Entity): entity is Type { - return "kind" in entity && entity.kind !== "Value" && entity.kind !== "ParamConstraintUnion"; + return "kind" in entity && entity.kind !== "Value" && entity.kind !== "MixedConstraint"; } export function isValue(entity: Entity): entity is Value { return "valueKind" in entity; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 5cd52d5442..f0808de238 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -106,7 +106,7 @@ export interface TemplatedTypeBase { * - Values * - Value Constraints */ -export type Entity = Type | Value | ValueType | ParamConstraintUnion; +export type Entity = Type | Value | ValueConstraint | MixedConstraint; export type Type = | BooleanLiteral @@ -171,9 +171,16 @@ export interface Projector { projectedGlobalNamespace?: Namespace; } -export interface ValueType { - kind: "Value"; - target: Type; +export interface ValueConstraint { + readonly kind: "Value"; + readonly target: Type; +} + +export interface MixedConstraint { + readonly kind: "MixedConstraint"; + readonly node: UnionExpressionNode; + + readonly options: (Type | ValueConstraint)[]; } export interface IntrinsicType extends BaseType { @@ -611,13 +618,6 @@ export interface Tuple extends BaseType { values: Type[]; } -export interface ParamConstraintUnion { - kind: "ParamConstraintUnion"; // TODO: review naming - node: UnionExpressionNode; - - readonly options: (Type | ValueType)[]; -} - export interface Union extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Union"; name?: string; @@ -655,7 +655,7 @@ export interface UnionVariant extends BaseType, DecoratedType { export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; - constraint?: Type | ParamConstraintUnion | ValueType; + constraint?: Type | MixedConstraint | ValueConstraint; default?: Type | Value; } @@ -683,7 +683,7 @@ export interface FunctionParameter extends BaseType { kind: "FunctionParameter"; node: FunctionParameterNode; name: string; - type: Type | ParamConstraintUnion | ValueType; + type: Type | MixedConstraint | ValueConstraint; optional: boolean; rest: boolean; } diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index c017ea9c27..9f45288824 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -15,7 +15,7 @@ import { SyntaxKind, Type, UnionVariant, - ValueType, + ValueConstraint, } from "../core/types.js"; import { printId } from "../formatter/print/printer.js"; @@ -30,7 +30,7 @@ export function getSymbolSignature(program: Program, sym: Sym): string { return getTypeSignature(type); } -function getTypeSignature(type: Type | ValueType): string { +function getTypeSignature(type: Type | ValueConstraint): string { switch (type.kind) { case "Scalar": case "Enum": diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index f852f09bdc..adbbf7580b 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -2,13 +2,13 @@ import { DocTag, FunctionParameter, IntrinsicScalarName, + MixedConstraint, Model, - ParamConstraintUnion, Program, Scalar, SyntaxKind, Type, - ValueType, + ValueConstraint, getSourceLocation, isArrayModelType, isUnknownType, @@ -115,7 +115,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } } - function getRestTSParmeterType(type: Type | ValueType | ParamConstraintUnion) { + function getRestTSParmeterType(type: Type | ValueConstraint | MixedConstraint) { if (type.kind === "Value") { if (type.target.kind === "Model" && isArrayModelType(program, type.target)) { return `(${getValueTSType(type.target.indexer.value)})[]`; @@ -131,7 +131,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } function getTSParmeterType( - type: Type | ValueType | ParamConstraintUnion, + type: Type | ValueConstraint | MixedConstraint, isTarget?: boolean ): string { if (type.kind === "Value") { diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 4499238a2c..0501930ba9 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -14,11 +14,11 @@ import { TemplateParameterDeclarationNode, Type, UnionVariant, - ValueType, + ValueConstraint, } from "@typespec/compiler"; /** @internal */ -export function getTypeSignature(type: Type | ValueType): string { +export function getTypeSignature(type: Type | ValueConstraint): string { if (type.kind === "Value") { return `valueof ${getTypeSignature(type.target)}`; } From d4d778b665982b70c6bad2e205653a6690d44a36 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 19 Apr 2024 20:13:08 -0700 Subject: [PATCH 122/184] Structured mixed constraint --- packages/compiler/src/core/checker.ts | 110 +++++++++++++++--- .../src/core/helpers/type-name-utils.ts | 6 +- packages/compiler/src/core/types.ts | 6 +- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3fc561b914..e7f97e7d42 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -908,6 +908,7 @@ export function createChecker(program: Program): Checker { return entity; } + // TODO: do we still need this? /** Extact the type constraint a value should match. */ function extractValueOfConstraints( constraint: CheckConstraint | undefined @@ -917,11 +918,10 @@ export function createChecker(program: Program): Checker { } if (constraint.constraint.kind === "Value") { return { kind: constraint.kind, type: constraint.constraint.target }; + } else if (constraint.constraint.value) { + return { kind: constraint.kind, type: constraint.constraint.value.target }; } else { - const valueOfOptions = constraint.constraint.options - .filter((x): x is ValueConstraint => x.kind === "Value") - .map((x) => x.target); - return { kind: constraint.kind, type: createUnion(valueOfOptions) }; + return undefined; } } @@ -1745,7 +1745,7 @@ export function createChecker(program: Program): Checker { } /** Check a union expresion used in a parameter constraint, those allow the use of `valueof` as a variant. */ - function checkUnionExpressionAsParamConstraint( + function checkMixedConstraint( node: UnionExpressionNode, mapper: TypeMapper | undefined ): Union | MixedConstraint { @@ -1754,13 +1754,63 @@ export function createChecker(program: Program): Checker { return checkUnionExpression(node, mapper); } + const values: Type[] = []; + const types: Type[] = []; + for (const option of node.options) { + const entity = getTypeOrValueOfTypeForNode(option, mapper); + if (entity.kind === "Value") { + values.push(entity.target); + } else { + types.push(entity); + } + } return { kind: "MixedConstraint", node, - options: node.options.map((x) => getTypeOrValueOfTypeForNode(x, mapper)), + value: + values.length === 0 + ? undefined + : { + kind: "Value", + target: values.length === 1 ? values[0] : createConstraintUnion(node, values), + }, + type: + types.length === 0 + ? undefined + : types.length === 1 + ? types[0] + : createConstraintUnion(node, types), }; } + function createConstraintUnion(node: UnionExpressionNode, options: Type[]): Union { + const variants = createRekeyableMap(); + const union: Union = createAndFinishType({ + kind: "Union", + node, + options, + decorators: [], + variants, + expression: true, + }); + + for (const option of options) { + const name = Symbol("indexer-union-variant"); + variants.set( + name, + createAndFinishType({ + kind: "UnionVariant", + node: undefined, + type: option, + name, + union, + decorators: [], + }) + ); + } + return union; + } + function checkUnionExpression(node: UnionExpressionNode, mapper: TypeMapper | undefined): Union { const unionType: Union = createAndFinishType({ kind: "Union", @@ -2004,7 +2054,7 @@ export function createChecker(program: Program): Checker { ): Type | MixedConstraint | ValueConstraint { switch (node.kind) { case SyntaxKind.UnionExpression: - return checkUnionExpressionAsParamConstraint(node, mapper); + return checkMixedConstraint(node, mapper); default: return getTypeOrValueOfTypeForNode(node, mapper); } @@ -6729,12 +6779,7 @@ export function createChecker(program: Program): Checker { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } if (target.kind === "MixedConstraint") { - return isAssignableToParameterConstraintUnion( - source, - target, - diagnosticTarget, - relationCache - ); + return isAssignableToMixedConstraint(source, target, diagnosticTarget, relationCache); } if (isValue(source) || source.kind === "Value" || source.kind === "MixedConstraint") { @@ -6841,16 +6886,27 @@ export function createChecker(program: Program): Checker { return isValueOfTypeInternal(source, target.target, diagnosticTarget, relationCache); } - function isAssignableToParameterConstraintUnion( + function isAssignableToMixedConstraint( source: Entity, target: MixedConstraint, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { if ("kind" in source && source.kind === "MixedConstraint") { - for (const option of source.options) { - const [variantAssignable] = isAssignableToParameterConstraintUnion( - option, + if (source.type) { + const [variantAssignable] = isAssignableToMixedConstraint( + source.type, + target, + diagnosticTarget, + relationCache + ); + if (!variantAssignable) { + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + } + } + if (source.value) { + const [variantAssignable] = isAssignableToMixedConstraint( + source.value, target, diagnosticTarget, relationCache @@ -6862,8 +6918,24 @@ export function createChecker(program: Program): Checker { return [Related.true, []]; } - for (const option of target.options) { - const [related] = isTypeAssignableToInternal(source, option, diagnosticTarget, relationCache); + if (target.type) { + const [related] = isTypeAssignableToInternal( + source, + target.type, + diagnosticTarget, + relationCache + ); + if (related) { + return [Related.true, []]; + } + } + if (target.value) { + const [related] = isTypeAssignableToInternal( + source, + target.value, + diagnosticTarget, + relationCache + ); if (related) { return [Related.true, []]; } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 8ddaeaa645..38670a28d1 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -1,4 +1,5 @@ import { printId } from "../../formatter/print/printer.js"; +import { isDefined } from "../../utils/misc.js"; import { isTemplateInstance, isValue } from "../type-utils.js"; import type { Entity, @@ -90,7 +91,10 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string case "Value": return `valueof ${getTypeName(entity.target, options)}`; case "MixedConstraint": - return entity.options.map((x) => getEntityName(x, options)).join(" | "); + return [entity.type, entity.value] + .filter(isDefined) + .map((x) => getEntityName(x, options)) + .join(" | "); default: return getTypeName(entity, options); } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index f0808de238..fc0d1a5147 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -180,7 +180,11 @@ export interface MixedConstraint { readonly kind: "MixedConstraint"; readonly node: UnionExpressionNode; - readonly options: (Type | ValueConstraint)[]; + /** Type constraints */ + readonly type?: Type; + + /** Expecting value */ + readonly value?: ValueConstraint; } export interface IntrinsicType extends BaseType { From 02de1e7819c991144306597f8186cca2d88271b1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 13:37:23 -0700 Subject: [PATCH 123/184] Mixed function parameter --- packages/compiler/src/core/checker.ts | 177 ++++++++++-------- .../src/core/helpers/string-template-utils.ts | 4 +- packages/compiler/src/core/messages.ts | 2 +- packages/compiler/src/core/semantic-walker.ts | 2 +- packages/compiler/src/core/types.ts | 28 ++- .../compiler/src/formatter/print/printer.ts | 2 +- .../compiler/test/checker/decorators.test.ts | 6 +- .../compiler/test/checker/relation.test.ts | 163 +++++++--------- .../test/checker/values/scalar-values.test.ts | 6 +- .../test/decorators/decorators.test.ts | 16 +- .../compiler/test/decorators/service.test.ts | 5 +- packages/http/test/http-decorators.test.ts | 15 -- packages/openapi/test/decorators.test.ts | 2 - .../diagnostics.txt | 4 +- .../scenarios/options-invalid/diagnostics.txt | 2 +- 15 files changed, 219 insertions(+), 215 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e7f97e7d42..7132263be5 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -108,6 +108,7 @@ import { MemberNode, MemberType, MixedConstraint, + MixedFunctionParameter, Model, ModelExpressionNode, ModelIndexer, @@ -156,6 +157,7 @@ import { ScalarConstructorNode, ScalarStatementNode, ScalarValue, + SigFunctionParameter, StdTypeName, StdTypes, StringLiteral, @@ -1428,12 +1430,7 @@ export function createChecker(program: Program): Checker { ); // TODO-TIM check if we expose this below - commit( - param, - param.constraint?.kind === "Value" || param.constraint?.kind === "MixedConstraint" - ? unknownType - : param.constraint ?? unknownType - ); + commit(param, param.constraint?.type ?? unknownType); } continue; @@ -1443,17 +1440,13 @@ export function createChecker(program: Program): Checker { if (param.constraint) { const constraint = - param.constraint.kind === "TemplateParameter" - ? finalMap.get(param.constraint)! + param.constraint.type?.kind === "TemplateParameter" + ? finalMap.get(param.constraint.type)! : param.constraint; if (isErrorType(type) || !checkTypeAssignable(type, constraint, argNode)) { // TODO-TIM check if we expose this below - const effectiveType = - param.constraint?.kind === "Value" || param.constraint.kind === "MixedConstraint" - ? unknownType - : param.constraint; - + const effectiveType = param.constraint.type ?? unknownType; commit(param, effectiveType); continue; } @@ -1745,15 +1738,10 @@ export function createChecker(program: Program): Checker { } /** Check a union expresion used in a parameter constraint, those allow the use of `valueof` as a variant. */ - function checkMixedConstraint( + function checkMixedConstraintUnion( node: UnionExpressionNode, mapper: TypeMapper | undefined - ): Union | MixedConstraint { - const hasValueOf = node.options.some((x) => x.kind === SyntaxKind.ValueOfExpression); - if (!hasValueOf) { - return checkUnionExpression(node, mapper); - } - + ): MixedConstraint { const values: Type[] = []; const types: Type[] = []; for (const option of node.options) { @@ -1907,8 +1895,8 @@ export function createChecker(program: Program): Checker { name: `@${name}`, namespace, node, - target: checkFunctionParameter(node.target, mapper), - parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper)), + target: checkFunctionParameter(node.target, mapper, true), + parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, true)), implementation: implementation ?? (() => {}), }); @@ -1924,9 +1912,11 @@ export function createChecker(program: Program): Checker { const marshalling = resolveDecoratorArgMarshalling(decorator); if (marshalling === "legacy") { for (const param of decorator.parameters) { - if (param.type.kind === "Value") { + if (param.type.value) { if ( - ignoreDiagnostics(isTypeAssignableTo(nullType, param.type.target, param.type.target)) + ignoreDiagnostics( + isTypeAssignableTo(nullType, param.type.value.target, param.type.value) + ) ) { reportDeprecated( program, @@ -1939,9 +1929,13 @@ export function createChecker(program: Program): Checker { ); } else if ( ignoreDiagnostics( - isTypeAssignableTo(param.type.target, getStdType("numeric"), param.type.target) + isTypeAssignableTo( + param.type.value.target, + getStdType("numeric"), + param.type.value.target + ) ) && - !canNumericConstraintBeJsNumber(param.type.target) + !canNumericConstraintBeJsNumber(param.type.value.target) ) { reportDeprecated( program, @@ -1989,7 +1983,7 @@ export function createChecker(program: Program): Checker { name, namespace, node, - parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper)), + parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, true)), returnType: node.returnType ? getTypeForNode(node.returnType, mapper) : unknownType, implementation: implementation ?? (() => {}), }); @@ -2003,7 +1997,18 @@ export function createChecker(program: Program): Checker { function checkFunctionParameter( node: FunctionParameterNode, - mapper: TypeMapper | undefined + mapper: TypeMapper | undefined, + mixed: true + ): MixedFunctionParameter; + function checkFunctionParameter( + node: FunctionParameterNode, + mapper: TypeMapper | undefined, + mixed: false + ): SigFunctionParameter; + function checkFunctionParameter( + node: FunctionParameterNode, + mapper: TypeMapper | undefined, + mixed: boolean ): FunctionParameter { const links = getSymbolLinks(node.symbol); @@ -2023,17 +2028,36 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "rest-parameter-array", target: node.type }) ); } - const type = node.type ? getParamConstraintEntityForNode(node.type) : unknownType; - const parameterType: FunctionParameter = createType({ + const base = { kind: "FunctionParameter", node, name: node.id.sv, optional: node.optional, rest: node.rest, - type, implementation: node.symbol.value!, - }); + } as const; + let parameterType: FunctionParameter; + + if (mixed) { + const type = node.type + ? getParamConstraintEntityForNode(node.type) + : ({ kind: "MixedConstraint", type: unknownType } satisfies MixedConstraint); + parameterType = createType({ + ...base, + type, + mixed: true, + implementation: node.symbol.value!, + }); + } else { + parameterType = createType({ + ...base, + mixed: false, + type: node.type ? getTypeForNode(node.type) : unknownType, + implementation: node.symbol.value!, + }); + } + linkType(links, parameterType, mapper); return parameterType; @@ -2048,15 +2072,18 @@ export function createChecker(program: Program): Checker { } } - function getParamConstraintEntityForNode( - node: Node, - mapper?: TypeMapper - ): Type | MixedConstraint | ValueConstraint { + function getParamConstraintEntityForNode(node: Expression, mapper?: TypeMapper): MixedConstraint { switch (node.kind) { case SyntaxKind.UnionExpression: - return checkMixedConstraint(node, mapper); + return checkMixedConstraintUnion(node, mapper); default: - return getTypeOrValueOfTypeForNode(node, mapper); + const entity = getTypeOrValueOfTypeForNode(node, mapper); + return { + kind: "MixedConstraint", + node: node, + type: entity.kind === "Value" ? undefined : entity, + value: entity.kind === "Value" ? entity : undefined, + }; } } @@ -3873,10 +3900,7 @@ export function createChecker(program: Program): Checker { for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { - const restType = - parameter.type.kind === "MixedConstraint" || parameter.type.kind === "Value" // TODO: change if we change this to not be a FunctionParameter - ? undefined - : getIndexType(parameter.type); + const restType = getIndexType(parameter.type); if (restType) { for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; @@ -4835,18 +4859,14 @@ export function createChecker(program: Program): Checker { } for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { - const restType = - parameter.type.kind === "MixedConstraint" - ? undefined - : getIndexType( - parameter.type.kind === "Value" ? parameter.type.target : parameter.type - ); + const restType = getIndexType( + parameter.type.value ? parameter.type.value.target : parameter.type.type! + ); if (restType) { - const perParamType = - parameter.type.kind === "Value" - ? ({ kind: "Value", target: restType } as const) - : restType; + const perParamType = parameter.type.value + ? ({ kind: "Value", target: restType } as const) + : restType; for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { @@ -5092,7 +5112,7 @@ export function createChecker(program: Program): Checker { scalar: parentScalar, name, node, - parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper)), + parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, false)), }); linkMapper(member, mapper); if (shouldCreateTypeForTemplate(node.parent!, mapper)) { @@ -6409,7 +6429,7 @@ export function createChecker(program: Program): Checker { } function createFunctionType(fn: (...args: Type[]) => Type): FunctionType { - const parameters: FunctionParameter[] = []; + const parameters: MixedFunctionParameter[] = []; return createType({ kind: "Function", name: "", @@ -6741,12 +6761,13 @@ export function createChecker(program: Program): Checker { "kind" in source && "kind" in target && source.kind === "TemplateParameter" && - source.constraint && - target.kind === "Value" + source.constraint?.type && + target.kind === "MixedConstraint" && + target.value ) { const [assignable] = isTypeAssignableToInternal( - source.constraint, - target.target, + source.constraint.type, + target.value.target, diagnosticTarget, relationCache ); @@ -6763,11 +6784,7 @@ export function createChecker(program: Program): Checker { } } - while ( - "kind" in source && - source.kind === "TemplateParameter" && - source.constraint !== source - ) { + if ("kind" in source && source.kind === "TemplateParameter") { source = source.constraint ?? unknownType; } @@ -6782,9 +6799,17 @@ export function createChecker(program: Program): Checker { return isAssignableToMixedConstraint(source, target, diagnosticTarget, relationCache); } - if (isValue(source) || source.kind === "Value" || source.kind === "MixedConstraint") { + if ( + isValue(source) || + source.kind === "Value" || + (source.kind === "MixedConstraint" && source.value) + ) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } + if (source.kind === "MixedConstraint") { + return isTypeAssignableToInternal(source.type!, target, diagnosticTarget, relationCache); + } + const isSimpleTypeRelated = isSimpleTypeAssignableTo(source, target); if (isSimpleTypeRelated === true) { return [Related.true, []]; @@ -6893,29 +6918,31 @@ export function createChecker(program: Program): Checker { relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { if ("kind" in source && source.kind === "MixedConstraint") { - if (source.type) { - const [variantAssignable] = isAssignableToMixedConstraint( + if (source.type && target.type) { + const [variantAssignable, diagnostics] = isTypeAssignableToInternal( source.type, - target, + target.type, diagnosticTarget, relationCache ); - if (!variantAssignable) { - return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + if (variantAssignable === Related.false) { + return [Related.false, diagnostics]; } + return [Related.true, []]; } - if (source.value) { - const [variantAssignable] = isAssignableToMixedConstraint( + if (source.value && target.value) { + const [variantAssignable, diagnostics] = isAssignableToValueType( source.value, - target, + target.value, diagnosticTarget, relationCache ); - if (!variantAssignable) { - return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + if (variantAssignable === Related.false) { + return [Related.false, diagnostics]; } + return [Related.true, []]; } - return [Related.true, []]; + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } if (target.type) { diff --git a/packages/compiler/src/core/helpers/string-template-utils.ts b/packages/compiler/src/core/helpers/string-template-utils.ts index 1cab20d741..8721c6865e 100644 --- a/packages/compiler/src/core/helpers/string-template-utils.ts +++ b/packages/compiler/src/core/helpers/string-template-utils.ts @@ -25,7 +25,7 @@ export function stringTemplateToString( case "StringTemplate": return diagnostics.pipe(stringTemplateToString(x.type)); case "TemplateParameter": - if (x.type.constraint && x.type.constraint.kind === "Value") { + if (x.type.constraint && x.type.constraint.value !== undefined) { return ""; } // eslint-disable-next-line no-fallthrough @@ -61,7 +61,7 @@ export function isStringTemplateSerializable( diagnostics.pipe(isStringTemplateSerializable(span.type)); break; case "TemplateParameter": - if (span.type.constraint && span.type.constraint.kind === "Value") { + if (span.type.constraint && span.type.constraint.value !== undefined) { break; // Value types will be serializable in the template instance. } // eslint-disable-next-line no-fallthrough diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index b2ae93c94f..1cadb41126 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -768,7 +768,7 @@ const diagnostics = { "invalid-argument": { severity: "error", messages: { - default: paramMessage`Argument '${"value"}' is not assignable to parameter of type '${"expected"}'`, + default: paramMessage`Argument of type '${"value"}' is not assignable to parameter of type '${"expected"}'`, }, }, "invalid-argument-count": { diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index e84ffadb78..622a80e6af 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -96,7 +96,7 @@ export function scopeNavigationToNamespace( return ListenerFlow.NoRecursion; } } - return callback(x as any); + return (callback as any)(x); }; } return wrappedListeners as any; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index fc0d1a5147..6b39c36241 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -178,7 +178,7 @@ export interface ValueConstraint { export interface MixedConstraint { readonly kind: "MixedConstraint"; - readonly node: UnionExpressionNode; + readonly node?: UnionExpressionNode | Expression; /** Type constraints */ readonly type?: Type; @@ -424,7 +424,7 @@ export interface ScalarConstructor extends BaseType { node: ScalarConstructorNode; name: string; scalar: Scalar; - parameters: FunctionParameter[]; + parameters: SigFunctionParameter[]; } export interface Interface extends BaseType, DecoratedType, TemplatedTypeBase { @@ -659,7 +659,7 @@ export interface UnionVariant extends BaseType, DecoratedType { export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; - constraint?: Type | MixedConstraint | ValueConstraint; + constraint?: MixedConstraint; default?: Type | Value; } @@ -668,8 +668,8 @@ export interface Decorator extends BaseType { node: DecoratorDeclarationStatementNode; name: `@${string}`; namespace: Namespace; - target: FunctionParameter; - parameters: FunctionParameter[]; + target: MixedFunctionParameter; + parameters: MixedFunctionParameter[]; implementation: (...args: unknown[]) => void; } @@ -678,20 +678,32 @@ export interface FunctionType extends BaseType { node?: FunctionDeclarationStatementNode; namespace?: Namespace; name: string; - parameters: FunctionParameter[]; + parameters: MixedFunctionParameter[]; returnType: Type; implementation: (...args: unknown[]) => unknown; } -export interface FunctionParameter extends BaseType { +export interface FunctionParameterBase extends BaseType { kind: "FunctionParameter"; node: FunctionParameterNode; name: string; - type: Type | MixedConstraint | ValueConstraint; optional: boolean; rest: boolean; } +/** Represent a function parameter that could accept types or values in the TypeSpec program. */ +export interface MixedFunctionParameter extends FunctionParameterBase { + // TODO: better name? + mixed: true; + type: MixedConstraint; +} +/** Represent a function parameter that represent the parameter signature(i.e the type would be the type of the value passed) */ +export interface SigFunctionParameter extends FunctionParameterBase { + mixed: false; + type: Type; +} +export type FunctionParameter = MixedFunctionParameter | SigFunctionParameter; + export interface Sym { readonly flags: SymbolFlags; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 57dd9645b5..14ca7178e6 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -1023,7 +1023,7 @@ export function printObjectLiteral( const hasProperties = node.properties && node.properties.length > 0; const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling); if (!hasProperties && !nodeHasComments) { - return "{}"; + return "#{}"; } const lineDoc = softline; const body: Doc[] = [ diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index f488d3b4ba..3b0efcc94a 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -281,7 +281,7 @@ describe("compiler: checker: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", + message: "Argument of type '123' is not assignable to parameter of type 'string'", }); expectDecoratorNotCalled(); }); @@ -297,11 +297,11 @@ describe("compiler: checker: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", + message: "Argument of type '123' is not assignable to parameter of type 'string'", }, { code: "invalid-argument", - message: "Argument '456' is not assignable to parameter of type 'string'", + message: "Argument of type '456' is not assignable to parameter of type 'string'", }, ]); expectDecoratorNotCalled(); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index ae915a2d91..ba3e39403a 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1,12 +1,11 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { - ConstStatementNode, Diagnostic, FunctionParameterNode, Model, - SyntaxKind, Type, + defineModuleFlags, } from "../../src/core/index.js"; import { BasicTestRunner, @@ -17,7 +16,6 @@ import { expectDiagnosticEmpty, expectDiagnostics, extractCursor, - resolveVirtualPath, } from "../../src/testing/index.js"; interface RelatedTypeOptions { @@ -40,6 +38,9 @@ describe("compiler: checker: type relations", () => { expectedDiagnosticPos: number; }> { host.addJsFile("mock.js", { + $flags: defineModuleFlags({ + decoratorArgMarshalling: "lossless", + }), $mock: () => null, }); const { source: code, pos } = extractCursor(` @@ -62,42 +63,24 @@ describe("compiler: checker: type relations", () => { return { related, diagnostics, expectedDiagnosticPos: pos }; } - async function checkValueAssignable({ source, target, commonCode }: RelatedTypeOptions): Promise<{ + async function checkValueAssignableToConstraint({ + source, + target, + commonCode, + }: RelatedTypeOptions): Promise<{ related: boolean; diagnostics: readonly Diagnostic[]; expectedDiagnosticPos: number; }> { const cursor = source.includes("┆") ? "" : "┆"; - host.addJsFile("mock.js", { $mock: () => null }); const { source: code, pos } = extractCursor(` - import "./mock.js"; - ${commonCode ?? ""} - extern dec mock(target: unknown, target: ${target}); - - const Source = ${cursor}${source}; + ${commonCode ?? ""} + model Test {} + alias Case = Test<${cursor}${source}>; `); - await runner.compile(code); - const constStatement = runner.program.sourceFiles - .get(resolveVirtualPath("main.tsp")) - ?.statements.find( - (x): x is ConstStatementNode => x.kind === SyntaxKind.ConstStatement && x.id.sv === "Source" - ); - ok(constStatement); - const value = runner.program.checker.getValueForNode(constStatement.value); - ok(value, `Could not find source type for ${source}`); - const decDeclaration = runner.program - .getGlobalNamespaceType() - .decoratorDeclarations.get("mock"); - const targetProp = decDeclaration?.parameters[0].type; - ok(targetProp, `Could not find target type for ${target}`); - - const [related, diagnostics] = runner.program.checker.isTypeAssignableTo( - value, - targetProp, - constStatement.value - ); - return { related, diagnostics, expectedDiagnosticPos: pos }; + const diagnostics = await runner.diagnose(code); + return { related: diagnostics.length === 0, diagnostics, expectedDiagnosticPos: pos }; } async function expectTypeAssignable(options: RelatedTypeOptions) { @@ -112,14 +95,18 @@ describe("compiler: checker: type relations", () => { expectDiagnostics(diagnostics, { ...match, pos: expectedDiagnosticPos }); } - async function expectValueAssignable(options: RelatedTypeOptions) { - const { related, diagnostics } = await checkValueAssignable(options); + async function expectValueAssignableToConstraint(options: RelatedTypeOptions) { + const { related, diagnostics } = await checkValueAssignableToConstraint(options); expectDiagnosticEmpty(diagnostics); ok(related, `Value ${options.source} should be assignable to ${options.target}`); } - async function expectValueNotAssignable(options: RelatedTypeOptions, match: DiagnosticMatch) { - const { related, diagnostics, expectedDiagnosticPos } = await checkValueAssignable(options); + async function expectValueNotAssignableToConstraint( + options: RelatedTypeOptions, + match: DiagnosticMatch + ) { + const { related, diagnostics, expectedDiagnosticPos } = + await checkValueAssignableToConstraint(options); ok(!related, `Value ${options.source} should NOT be assignable to ${options.target}`); expectDiagnostics(diagnostics, { ...match, pos: expectedDiagnosticPos }); } @@ -1276,15 +1263,15 @@ describe("compiler: checker: type relations", () => { describe("Value constraint", () => { describe("valueof string", () => { it("can assign string literal", async () => { - await checkValueAssignable({ source: `"foo bar"`, target: "string" }); + await checkValueAssignableToConstraint({ source: `"foo bar"`, target: "string" }); }); it("cannot assign numeric literal", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `123`, target: "valueof string" }, { - code: "unassignable", - message: "Type '123' is not assignable to type 'string'", + code: "invalid-argument", + message: "Argument of type '123' is not assignable to parameter of type 'string'", } ); }); @@ -1302,15 +1289,15 @@ describe("compiler: checker: type relations", () => { describe("valueof boolean", () => { it("can assign boolean literal", async () => { - await expectValueAssignable({ source: `true`, target: "valueof boolean" }); + await expectValueAssignableToConstraint({ source: `true`, target: "valueof boolean" }); }); it("cannot assign numeric literal", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `123`, target: "valueof boolean" }, { - code: "unassignable", - message: "Type '123' is not assignable to type 'boolean'", + code: "invalid-argument", + message: "Argument of type '123' is not assignable to parameter of type 'boolean'", } ); }); @@ -1328,7 +1315,7 @@ describe("compiler: checker: type relations", () => { describe("valueof int16", () => { it("can assign int16 literal", async () => { - await expectValueAssignable({ source: `12`, target: "valueof int16" }); + await expectValueAssignableToConstraint({ source: `12`, target: "valueof int16" }); }); it("can assign valueof int8", async () => { @@ -1336,31 +1323,31 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign int too large", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `123456`, target: "valueof int16" }, { - code: "unassignable", - message: "Type '123456' is not assignable to type 'int16'", + code: "invalid-argument", + message: "Argument of type '123456' is not assignable to parameter of type 'int16'", } ); }); it("cannot assign float", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `12.6`, target: "valueof int16" }, { - code: "unassignable", - message: "Type '12.6' is not assignable to type 'int16'", + code: "invalid-argument", + message: "Argument of type '12.6' is not assignable to parameter of type 'int16'", } ); }); it("cannot assign string literal", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `"foo bar"`, target: "valueof int16" }, { - code: "unassignable", - message: `Type '"foo bar"' is not assignable to type 'int16'`, + code: "invalid-argument", + message: `Argument of type '"foo bar"' is not assignable to parameter of type 'int16'`, } ); }); @@ -1378,15 +1365,15 @@ describe("compiler: checker: type relations", () => { describe("valueof float32", () => { it("can assign float32 literal", async () => { - await expectValueAssignable({ source: `12.6`, target: "valueof float32" }); + await expectValueAssignableToConstraint({ source: `12.6`, target: "valueof float32" }); }); it("cannot assign string literal", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `"foo bar"`, target: "valueof float32" }, { - code: "unassignable", - message: `Type '"foo bar"' is not assignable to type 'float32'`, + code: "invalid-argument", + message: `Argument of type '"foo bar"' is not assignable to parameter of type 'float32'`, } ); }); @@ -1404,7 +1391,7 @@ describe("compiler: checker: type relations", () => { describe("valueof model", () => { it("can assign object literal", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#{name: "foo"}`, target: "valueof Info", commonCode: `model Info { name: string }`, @@ -1412,7 +1399,7 @@ describe("compiler: checker: type relations", () => { }); it("can assign object literal with optional properties", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#{name: "foo"}`, target: "valueof Info", commonCode: `model Info { name: string, age?: int32 }`, @@ -1420,7 +1407,7 @@ describe("compiler: checker: type relations", () => { }); it("can assign object literal with additional properties", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#{age: 21, name: "foo"}`, target: "valueof Info", commonCode: `model Info { age: int32, ...Record }`, @@ -1444,21 +1431,21 @@ describe("compiler: checker: type relations", () => { describe("excess properties", () => { it("emit diagnostic when using extra properties", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { - source: `#{name: "foo", ┆notDefined: "bar"}`, + source: `#{name: "foo", notDefined: "bar"}`, target: "valueof Info", commonCode: `model Info { name: string }`, }, { - code: "unexpected-property", - message: `Object literal may only specify known properties, and 'notDefined' does not exist in type 'Info'.`, + code: "invalid-argument", + message: `Argument of type '{ name: "foo", notDefined: "bar" }' is not assignable to parameter of type 'Info'`, } ); }); it("don't emit diagnostic when the extra props are spread into it", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#{name: "foo", ...common}`, target: "valueof Info", commonCode: ` @@ -1470,15 +1457,15 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign a tuple literal", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `#["foo"]`, target: "valueof Info", commonCode: `model Info { name: string }`, }, { - code: "unassignable", - message: `Type '["foo"]' is not assignable to type 'Info'`, + code: "invalid-argument", + message: `Argument of type '["foo"]' is not assignable to parameter of type 'Info'`, } ); }); @@ -1496,14 +1483,14 @@ describe("compiler: checker: type relations", () => { describe("valueof array", () => { it("can assign tuple literal", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#["foo"]`, target: "valueof string[]", }); }); it("can assign tuple literal of object literal", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#[#{name: "a"}, #{name: "b"}]`, target: "valueof Info[]", commonCode: `model Info { name: string }`, @@ -1512,7 +1499,7 @@ describe("compiler: checker: type relations", () => { // Disabled for now as this is allowed for backcompat it.skip("cannot assign a tuple", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `["foo"]`, target: "valueof string[]", @@ -1525,14 +1512,14 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign an object literal", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `#{name: "foo"}`, target: "valueof string[]", }, { - code: "unassignable", - message: `Type '{ name: "foo" }' is not assignable to type 'string[]'`, + code: "invalid-argument", + message: `Argument of type '{ name: "foo" }' is not assignable to parameter of type 'string[]'`, } ); }); @@ -1550,40 +1537,34 @@ describe("compiler: checker: type relations", () => { describe("valueof tuple", () => { it("can assign tuple literal", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#["foo", 12]`, target: "valueof [string, int32]", }); }); it("cannot assign tuple literal with too few values", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `#["foo"]`, target: "valueof [string, string]", }, { - code: "unassignable", - message: [ - `Type '["foo"]' is not assignable to type '[string, string]'`, - " Source has 1 element(s) but target requires 2.", - ].join("\n"), + code: "invalid-argument", + message: `Argument of type '["foo"]' is not assignable to parameter of type '[string, string]'`, } ); }); it("cannot assign tuple literal with too many values", async () => { - await expectValueNotAssignable( + await expectValueNotAssignableToConstraint( { source: `#["a", "b", "c"]`, target: "valueof [string, string]", }, { - code: "unassignable", - message: [ - `Type '["a", "b", "c"]' is not assignable to type '[string, string]'`, - " Source has 3 element(s) but target requires 2.", - ].join("\n"), + code: "invalid-argument", + message: `Argument of type '["a", "b", "c"]' is not assignable to parameter of type '[string, string]'`, } ); }); @@ -1591,13 +1572,13 @@ describe("compiler: checker: type relations", () => { describe("valueof union", () => { it("can assign tuple literal variant", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `#["foo", 12]`, target: "valueof ([string, int32] | string | boolean)", }); }); it("can assign string variant", async () => { - await expectValueAssignable({ + await expectValueAssignableToConstraint({ source: `"foo"`, target: "valueof ([string, int32] | string | boolean)", }); @@ -1649,7 +1630,7 @@ describe("compiler: checker: type relations", () => { ["#[]", "unknown[]"], ["#[]", "unknown"], ])(`%s => %s`, async (source, target) => { - await expectValueNotAssignable({ source, target }, { code: "unassignable" }); + await expectValueNotAssignableToConstraint({ source, target }, { code: "unassignable" }); }); }); @@ -1668,7 +1649,7 @@ describe("compiler: checker: type relations", () => { ["#{}", "(valueof unknown) | unknown"], ["#{}", "(valueof {}) | {}"], ])(`%s => %s`, async (source, target) => { - await expectValueAssignable({ source, target }); + await expectValueAssignableToConstraint({ source, target }); }); it.each([ ["{}", "(valueof unknown) | unknown"], diff --git a/packages/compiler/test/checker/values/scalar-values.test.ts b/packages/compiler/test/checker/values/scalar-values.test.ts index fc5315ae5b..cb01e8ea20 100644 --- a/packages/compiler/test/checker/values/scalar-values.test.ts +++ b/packages/compiler/test/checker/values/scalar-values.test.ts @@ -73,7 +73,7 @@ describe("instantiate with named constructor", () => { const diagnostics = await diagnoseValue(`ipv4.fromString(123)`, ipv4Code); expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", + message: "Argument of type '123' is not assignable to parameter of type 'string'", }); }); @@ -132,7 +132,7 @@ describe("instantiate with named constructor", () => { ); expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", + message: "Argument of type '123' is not assignable to parameter of type 'string'", }); }); }); @@ -176,7 +176,7 @@ describe("instantiate with named constructor", () => { ); expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", + message: "Argument of type '123' is not assignable to parameter of type 'string'", }); }); }); diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 2d44b3ad5d..e12a82530e 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -222,7 +222,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'string'`, + message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -267,7 +267,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'string'`, + message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); @@ -320,7 +320,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'string'`, + message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -347,7 +347,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'string'`, + message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -503,7 +503,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument 'Foo' is not assignable to parameter of type 'Enum'", + message: "Argument of type 'Foo' is not assignable to parameter of type 'Enum'", }); }); }); @@ -520,7 +520,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '4' is not assignable to parameter of type 'string'", + message: "Argument of type '4' is not assignable to parameter of type 'string'", }, ]); }); @@ -708,7 +708,7 @@ describe("compiler: built-in decorators", () => { '"int32"', // TODO: Arguably this should be improved. "invalid-argument", - `Argument '"int32"' is not assignable to parameter of type 'Scalar'`, + `Argument of type '"int32"' is not assignable to parameter of type 'Scalar'`, ], ]; describe("valid", () => { @@ -834,7 +834,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '"foo"' is not assignable to parameter of type 'Operation'`, + message: `Argument of type '"foo"' is not assignable to parameter of type 'Operation'`, severity: "error", }); }); diff --git a/packages/compiler/test/decorators/service.test.ts b/packages/compiler/test/decorators/service.test.ts index 8e36458415..02351e769c 100644 --- a/packages/compiler/test/decorators/service.test.ts +++ b/packages/compiler/test/decorators/service.test.ts @@ -64,7 +64,8 @@ describe("compiler: service", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '{ title: 123 }' is not assignable to parameter of type 'ServiceOptions'", + message: + "Argument of type '{ title: 123 }' is not assignable to parameter of type 'ServiceOptions'", }); }); @@ -102,7 +103,7 @@ describe("compiler: service", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", message: - "Argument '{ version: 123 }' is not assignable to parameter of type 'ServiceOptions'", + "Argument of type '{ version: 123 }' is not assignable to parameter of type 'ServiceOptions'", }); }); }); diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index e23ccf49f0..c2f0043205 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -81,17 +81,12 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: - "Argument '123' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", }, { code: "invalid-argument", - message: - "Argument '{ name: 123 }' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'", }, { code: "invalid-argument", - message: `Argument '{ format: "invalid" }' is not assignable to parameter of type 'string | TypeSpec.Http.HeaderOptions'`, }, ]); }); @@ -169,16 +164,12 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: - "Argument '123' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'", }, { code: "invalid-argument", - message: `Argument '{ name: 123 }' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'`, }, { code: "invalid-argument", - message: `Argument '{ format: "invalid" }' is not assignable to parameter of type 'string | TypeSpec.Http.QueryOptions'`, }, ]); }); @@ -306,7 +297,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: `Argument '{ shared: "yes" }' is not assignable to parameter of type '{ shared: boolean }'`, }, ]); }); @@ -361,7 +351,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", }, ]); }); @@ -615,7 +604,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", }); }); @@ -627,7 +615,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'string'", }); }); @@ -651,7 +638,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'Record'", }); }); @@ -711,7 +697,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument 'anOp' is not assignable to parameter of type '{} | Union | {}[]'", }); }); diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index 30c349d87e..b27b66f8b3 100644 --- a/packages/openapi/test/decorators.test.ts +++ b/packages/openapi/test/decorators.test.ts @@ -166,8 +166,6 @@ describe("openapi: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: - "Argument '123' is not assignable to parameter of type 'TypeSpec.OpenAPI.AdditionalInfo'", }); }); diff --git a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt index f14f8c09d9..aa7b924282 100644 --- a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt +++ b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt @@ -1,2 +1,2 @@ -/test/main.tsp:13:34 - error invalid-argument: Argument 'string' is not assignable to parameter of type 'valueof string | [uint32, uint32] | uint32' -/test/main.tsp:13:42 - error invalid-argument: Argument 'uint32' is not assignable to parameter of type 'valueof string | [uint32, uint32] | uint32' +/test/main.tsp:13:34 - error invalid-argument: Argument of type 'string' is not assignable to parameter of type 'valueof string | [uint32, uint32] | uint32' +/test/main.tsp:13:42 - error invalid-argument: Argument of type 'uint32' is not assignable to parameter of type 'valueof string | [uint32, uint32] | uint32' diff --git a/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt b/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt index 9d8531474f..18f7a4795a 100644 --- a/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt +++ b/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt @@ -1 +1 @@ -/test/main.tsp:5:10 - error invalid-argument: Argument '{ name: "com.azure.Test", options: { java_package: {} } }' is not assignable to parameter of type 'TypeSpec.Protobuf.PackageDetails' +/test/main.tsp:5:10 - error invalid-argument: Argument of type '{ name: "com.azure.Test", options: { java_package: {} } }' is not assignable to parameter of type 'TypeSpec.Protobuf.PackageDetails' From e8e402472776d281203d1f753f9b18e00862f0c5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 13:51:23 -0700 Subject: [PATCH 124/184] fix tspd --- .../decorators-signatures.ts | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index adbbf7580b..d67e61e03c 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -1,8 +1,8 @@ import { DocTag, - FunctionParameter, IntrinsicScalarName, MixedConstraint, + MixedFunctionParameter, Model, Program, Scalar, @@ -105,7 +105,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat ].join(""); } - function getTSParameter(param: FunctionParameter, isTarget?: boolean): string { + function getTSParameter(param: MixedFunctionParameter, isTarget?: boolean): string { const optional = param.optional ? "?" : ""; const rest = param.rest ? "..." : ""; if (rest) { @@ -115,28 +115,48 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } } - function getRestTSParmeterType(type: Type | ValueConstraint | MixedConstraint) { - if (type.kind === "Value") { - if (type.target.kind === "Model" && isArrayModelType(program, type.target)) { - return `(${getValueTSType(type.target.indexer.value)})[]`; + function getRestTSParmeterType(constraint: MixedConstraint) { + let value: ValueConstraint | undefined; + let type: Type | undefined; + if (constraint.value) { + if ( + constraint.value.target.kind === "Model" && + isArrayModelType(program, constraint.value.target) + ) { + value = { kind: "Value", target: constraint.value.target.indexer.value }; } else { return "unknown"; } } - if (!(type.kind === "Model" && isArrayModelType(program, type))) { - return `unknown`; + if (constraint.type) { + if (constraint.type.kind === "Model" && isArrayModelType(program, constraint.type)) { + type = constraint.type.indexer.value; + } else { + return "unknown"; + } } - return `${getTSParmeterType(type.indexer.value)}[]`; + return `(${getTSParmeterType({ + kind: "MixedConstraint", + type, + value, + })})[]`; } - function getTSParmeterType( - type: Type | ValueConstraint | MixedConstraint, - isTarget?: boolean - ): string { - if (type.kind === "Value") { - return getValueTSType(type.target); + function getTSParmeterType(constraint: MixedConstraint, isTarget?: boolean): string { + if (constraint.type && constraint.value) { + return `${getTypeConstraintTSType(constraint.type, isTarget)} | ${getValueTSType(constraint.value.target)}`; + } + if (constraint.value) { + return getValueTSType(constraint.value.target); + } else if (constraint.type) { + return getTypeConstraintTSType(constraint.type, isTarget); } + + return useCompilerType("Type"); + } + + function getTypeConstraintTSType(type: Type, isTarget?: boolean): string { if (isTarget && isUnknownType(type)) { return useCompilerType("Type"); } @@ -146,7 +166,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat const variants = [...type.variants.values()]; if (isTarget) { - const items = [...new Set(variants.map((x) => getTSParmeterType(x.type, isTarget)))]; + const items = [...new Set(variants.map((x) => getTypeConstraintTSType(x.type, isTarget)))]; return items.join(" | "); } else if (variants.every((x) => isReflectionType(x.type))) { return variants.map((x) => useCompilerType((x.type as Model).name)).join(" | "); From 67f2eafe1eb0636d1f3014bfbbdd7d81d66493be Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 13:54:24 -0700 Subject: [PATCH 125/184] fix --- packages/http/test/http-decorators.test.ts | 3 --- packages/versioning/test/versioned-dependencies.test.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index c2f0043205..68a9ee9567 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -713,12 +713,9 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "unassignable", - message: `Type '"foo"' is not assignable to type 'TypeSpec.Http.AuthorizationCodeFlow | TypeSpec.Http.ImplicitFlow | TypeSpec.Http.PasswordFlow | TypeSpec.Http.ClientCredentialsFlow'`, }, { code: "unassignable", - message: - "Type 'Flow' is not assignable to type 'TypeSpec.Http.AuthorizationCodeFlow | TypeSpec.Http.ImplicitFlow | TypeSpec.Http.PasswordFlow | TypeSpec.Http.ClientCredentialsFlow'", }, ]); }); diff --git a/packages/versioning/test/versioned-dependencies.test.ts b/packages/versioning/test/versioned-dependencies.test.ts index 54e6130a9c..9d2acf401e 100644 --- a/packages/versioning/test/versioned-dependencies.test.ts +++ b/packages/versioning/test/versioned-dependencies.test.ts @@ -107,7 +107,7 @@ describe("versioning: reference versioned library", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", message: - "Argument '[[VersionedLib.Versions.l1, VersionedLib.Versions.l1]]' is not assignable to parameter of type 'EnumMember'", + "Argument of type '[[VersionedLib.Versions.l1, VersionedLib.Versions.l1]]' is not assignable to parameter of type 'EnumMember'", }); }); }); From fc2618ca907b9387b768ec7f5243466d7aa40c1e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 13:54:25 -0700 Subject: [PATCH 126/184] abc --- packages/versioning/test/versioned-dependencies.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/versioning/test/versioned-dependencies.test.ts b/packages/versioning/test/versioned-dependencies.test.ts index 9d2acf401e..73bb12ee7f 100644 --- a/packages/versioning/test/versioned-dependencies.test.ts +++ b/packages/versioning/test/versioned-dependencies.test.ts @@ -106,8 +106,6 @@ describe("versioning: reference versioned library", () => { `); expectDiagnostics(diagnostics, { code: "invalid-argument", - message: - "Argument of type '[[VersionedLib.Versions.l1, VersionedLib.Versions.l1]]' is not assignable to parameter of type 'EnumMember'", }); }); }); From bae371d3b04fb65f37d37e2c041a17fd55117c85 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 14:07:05 -0700 Subject: [PATCH 127/184] simplify --- packages/compiler/src/core/checker.ts | 48 +++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 7132263be5..d27003cc6a 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -881,7 +881,7 @@ export function createChecker(program: Program): Checker { interface CheckConstraint { kind: "argument" | "assignment"; - constraint: Type | ValueConstraint | MixedConstraint; + constraint: MixedConstraint; } interface CheckValueConstraint { kind: "argument" | "assignment"; @@ -915,12 +915,10 @@ export function createChecker(program: Program): Checker { function extractValueOfConstraints( constraint: CheckConstraint | undefined ): CheckValueConstraint | undefined { - if (constraint === undefined || isType(constraint.constraint)) { + if (constraint === undefined) { return undefined; } - if (constraint.constraint.kind === "Value") { - return { kind: constraint.kind, type: constraint.constraint.target }; - } else if (constraint.constraint.value) { + if (constraint.constraint.value) { return { kind: constraint.kind, type: constraint.constraint.value.target }; } else { return undefined; @@ -4830,7 +4828,7 @@ export function createChecker(program: Program): Checker { const jsMarshalling = resolveDecoratorArgMarshalling(declaration); function resolveArg( argNode: Expression, - perParamType: Type | ValueConstraint | MixedConstraint + perParamType: MixedConstraint ): DecoratorArgument | undefined { const arg = getTypeOrValueForNode(argNode, mapper, { kind: "argument", @@ -4859,18 +4857,13 @@ export function createChecker(program: Program): Checker { } for (const [index, parameter] of declaration.parameters.entries()) { if (parameter.rest) { - const restType = getIndexType( - parameter.type.value ? parameter.type.value.target : parameter.type.type! - ); + const restType = extractRestParamConstraint(parameter.type); if (restType) { - const perParamType = parameter.type.value - ? ({ kind: "Value", target: restType } as const) - : restType; for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { - const arg = resolveArg(argNode, perParamType); + const arg = resolveArg(argNode, restType); if (arg) { resolvedArgs.push(arg); } else { @@ -4894,6 +4887,35 @@ export function createChecker(program: Program): Checker { return [hasError, resolvedArgs]; } + /** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ + function extractRestParamConstraint(constraint: MixedConstraint): MixedConstraint | undefined { + let value: ValueConstraint | undefined; + let type: Type | undefined; + if (constraint.value) { + if ( + constraint.value.target.kind === "Model" && + isArrayModelType(program, constraint.value.target) + ) { + value = { kind: "Value", target: constraint.value.target.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 { + kind: "MixedConstraint", + type, + value, + }; + } + function getIndexType(type: Type): Type | undefined { return type.kind === "Model" ? type.indexer?.value : undefined; } From 1d2f418a58ddfd363028079a1c567bd077abea1b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 14:28:24 -0700 Subject: [PATCH 128/184] Don't need valueconstraint anymore --- packages/compiler/src/core/checker.ts | 117 ++++++------------ .../src/core/helpers/string-template-utils.ts | 4 +- .../src/core/helpers/type-name-utils.ts | 8 +- packages/compiler/src/core/type-utils.ts | 2 +- packages/compiler/src/core/types.ts | 9 +- .../decorators-signatures.ts | 39 +++--- 6 files changed, 71 insertions(+), 108 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d27003cc6a..8266426d02 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -198,8 +198,6 @@ import { UnionVariantNode, UnknownType, Value, - ValueConstraint, - ValueOfExpressionNode, VoidType, } from "./types.js"; @@ -918,8 +916,8 @@ export function createChecker(program: Program): Checker { if (constraint === undefined) { return undefined; } - if (constraint.constraint.value) { - return { kind: constraint.kind, type: constraint.constraint.value.target }; + if (constraint.constraint.valueType) { + return { kind: constraint.kind, type: constraint.constraint.valueType }; } else { return undefined; } @@ -1743,23 +1741,22 @@ export function createChecker(program: Program): Checker { const values: Type[] = []; const types: Type[] = []; for (const option of node.options) { - const entity = getTypeOrValueOfTypeForNode(option, mapper); - if (entity.kind === "Value") { - values.push(entity.target); + const [kind, type] = getTypeOrValueOfTypeForNode(option, mapper); + if (kind === "value") { + values.push(type); } else { - types.push(entity); + types.push(type); } } return { kind: "MixedConstraint", node, - value: + valueType: values.length === 0 ? undefined - : { - kind: "Value", - target: values.length === 1 ? values[0] : createConstraintUnion(node, values), - }, + : values.length === 1 + ? values[0] + : createConstraintUnion(node, values), type: types.length === 0 ? undefined @@ -1837,18 +1834,6 @@ export function createChecker(program: Program): Checker { return unionType; } - function checkValueOfExpression( - node: ValueOfExpressionNode, - mapper: TypeMapper | undefined - ): ValueConstraint { - const target = getTypeForNode(node.target, mapper); - - return { - kind: "Value", - target, - }; - } - /** * Intersection produces a model type from the properties of its operands. * So this doesn't work if we don't have a known set of properties (e.g. @@ -1910,12 +1895,8 @@ export function createChecker(program: Program): Checker { const marshalling = resolveDecoratorArgMarshalling(decorator); if (marshalling === "legacy") { for (const param of decorator.parameters) { - if (param.type.value) { - if ( - ignoreDiagnostics( - isTypeAssignableTo(nullType, param.type.value.target, param.type.value) - ) - ) { + if (param.type.valueType) { + if (ignoreDiagnostics(isTypeAssignableTo(nullType, param.type.valueType, param.type))) { reportDeprecated( program, [ @@ -1927,13 +1908,9 @@ export function createChecker(program: Program): Checker { ); } else if ( ignoreDiagnostics( - isTypeAssignableTo( - param.type.value.target, - getStdType("numeric"), - param.type.value.target - ) + isTypeAssignableTo(param.type.valueType, getStdType("numeric"), param.type.valueType) ) && - !canNumericConstraintBeJsNumber(param.type.value.target) + !canNumericConstraintBeJsNumber(param.type.valueType) ) { reportDeprecated( program, @@ -2061,12 +2038,13 @@ export function createChecker(program: Program): Checker { return parameterType; } - function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): Type | ValueConstraint { + function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): ["type" | "value", Type] { switch (node.kind) { case SyntaxKind.ValueOfExpression: - return checkValueOfExpression(node, mapper); + const target = getTypeForNode(node.target, mapper); + return ["value", target]; default: - return getTypeForNode(node, mapper); + return ["type", getTypeForNode(node, mapper)]; } } @@ -2075,12 +2053,12 @@ export function createChecker(program: Program): Checker { case SyntaxKind.UnionExpression: return checkMixedConstraintUnion(node, mapper); default: - const entity = getTypeOrValueOfTypeForNode(node, mapper); + const [kind, entity] = getTypeOrValueOfTypeForNode(node, mapper); return { kind: "MixedConstraint", node: node, - type: entity.kind === "Value" ? undefined : entity, - value: entity.kind === "Value" ? entity : undefined, + type: kind === "value" ? undefined : entity, + valueType: kind === "value" ? entity : undefined, }; } } @@ -4889,14 +4867,14 @@ export function createChecker(program: Program): Checker { /** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ function extractRestParamConstraint(constraint: MixedConstraint): MixedConstraint | undefined { - let value: ValueConstraint | undefined; + let valueType: Type | undefined; let type: Type | undefined; - if (constraint.value) { + if (constraint.valueType) { if ( - constraint.value.target.kind === "Model" && - isArrayModelType(program, constraint.value.target) + constraint.valueType.kind === "Model" && + isArrayModelType(program, constraint.valueType) ) { - value = { kind: "Value", target: constraint.value.target.indexer.value }; + valueType = constraint.valueType.indexer.value; } else { return undefined; } @@ -4912,7 +4890,7 @@ export function createChecker(program: Program): Checker { return { kind: "MixedConstraint", type, - value, + valueType, }; } @@ -6785,11 +6763,11 @@ export function createChecker(program: Program): Checker { source.kind === "TemplateParameter" && source.constraint?.type && target.kind === "MixedConstraint" && - target.value + target.valueType ) { const [assignable] = isTypeAssignableToInternal( source.constraint.type, - target.value.target, + target.valueType, diagnosticTarget, relationCache ); @@ -6811,9 +6789,6 @@ export function createChecker(program: Program): Checker { } if (source === target) return [Related.true, []]; - if ("kind" in target && target.kind === "Value") { - return isAssignableToValueType(source, target, diagnosticTarget, relationCache); - } if (isValue(target)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } @@ -6821,11 +6796,7 @@ export function createChecker(program: Program): Checker { return isAssignableToMixedConstraint(source, target, diagnosticTarget, relationCache); } - if ( - isValue(source) || - source.kind === "Value" || - (source.kind === "MixedConstraint" && source.value) - ) { + if (isValue(source) || (source.kind === "MixedConstraint" && source.valueType)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } if (source.kind === "MixedConstraint") { @@ -6912,25 +6883,15 @@ export function createChecker(program: Program): Checker { function isAssignableToValueType( source: Entity, - target: ValueConstraint, + target: Type, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - const isSourceAType = "kind" in source; - if (isSourceAType && source.kind === "Value") { - return isTypeAssignableToInternal( - source.target, - target.target, - diagnosticTarget, - relationCache - ); - } - if (!isValue(source)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - return isValueOfTypeInternal(source, target.target, diagnosticTarget, relationCache); + return isValueOfTypeInternal(source, target, diagnosticTarget, relationCache); } function isAssignableToMixedConstraint( @@ -6952,10 +6913,10 @@ export function createChecker(program: Program): Checker { } return [Related.true, []]; } - if (source.value && target.value) { - const [variantAssignable, diagnostics] = isAssignableToValueType( - source.value, - target.value, + if (source.valueType && target.valueType) { + const [variantAssignable, diagnostics] = isTypeAssignableToInternal( + source.valueType, + target.valueType, diagnosticTarget, relationCache ); @@ -6978,10 +6939,10 @@ export function createChecker(program: Program): Checker { return [Related.true, []]; } } - if (target.value) { - const [related] = isTypeAssignableToInternal( + if (target.valueType) { + const [related] = isAssignableToValueType( source, - target.value, + target.valueType, diagnosticTarget, relationCache ); diff --git a/packages/compiler/src/core/helpers/string-template-utils.ts b/packages/compiler/src/core/helpers/string-template-utils.ts index 8721c6865e..23342e6e18 100644 --- a/packages/compiler/src/core/helpers/string-template-utils.ts +++ b/packages/compiler/src/core/helpers/string-template-utils.ts @@ -25,7 +25,7 @@ export function stringTemplateToString( case "StringTemplate": return diagnostics.pipe(stringTemplateToString(x.type)); case "TemplateParameter": - if (x.type.constraint && x.type.constraint.value !== undefined) { + if (x.type.constraint && x.type.constraint.valueType !== undefined) { return ""; } // eslint-disable-next-line no-fallthrough @@ -61,7 +61,7 @@ export function isStringTemplateSerializable( diagnostics.pipe(isStringTemplateSerializable(span.type)); break; case "TemplateParameter": - if (span.type.constraint && span.type.constraint.value !== undefined) { + if (span.type.constraint && span.type.constraint.valueType !== undefined) { break; // Value types will be serializable in the template instance. } // eslint-disable-next-line no-fallthrough diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 38670a28d1..9fd73c6c94 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -88,12 +88,12 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string return getValuePreview(entity, options); } else { switch (entity.kind) { - case "Value": - return `valueof ${getTypeName(entity.target, options)}`; case "MixedConstraint": - return [entity.type, entity.value] + return [ + entity.type && getEntityName(entity.type), + entity.valueType && `valueof ${getEntityName(entity.valueType)}`, + ] .filter(isDefined) - .map((x) => getEntityName(x, options)) .join(" | "); default: return getTypeName(entity, options); diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index 9eb3537c3b..3eb197109c 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -44,7 +44,7 @@ export function isNullType(type: Entity): type is NullType { } export function isType(entity: Entity): entity is Type { - return "kind" in entity && entity.kind !== "Value" && entity.kind !== "MixedConstraint"; + return "kind" in entity && entity.kind !== "MixedConstraint"; } export function isValue(entity: Entity): entity is Value { return "valueKind" in entity; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 6b39c36241..908b1ca41b 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -106,7 +106,7 @@ export interface TemplatedTypeBase { * - Values * - Value Constraints */ -export type Entity = Type | Value | ValueConstraint | MixedConstraint; +export type Entity = Type | Value | MixedConstraint; export type Type = | BooleanLiteral @@ -171,11 +171,6 @@ export interface Projector { projectedGlobalNamespace?: Namespace; } -export interface ValueConstraint { - readonly kind: "Value"; - readonly target: Type; -} - export interface MixedConstraint { readonly kind: "MixedConstraint"; readonly node?: UnionExpressionNode | Expression; @@ -184,7 +179,7 @@ export interface MixedConstraint { readonly type?: Type; /** Expecting value */ - readonly value?: ValueConstraint; + readonly valueType?: Type; } export interface IntrinsicType extends BaseType { diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index d67e61e03c..f45fcbc7d8 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -8,7 +8,6 @@ import { Scalar, SyntaxKind, Type, - ValueConstraint, getSourceLocation, isArrayModelType, isUnknownType, @@ -115,40 +114,48 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } } - function getRestTSParmeterType(constraint: MixedConstraint) { - let value: ValueConstraint | undefined; + /** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ + function extractRestParamConstraint(constraint: MixedConstraint): MixedConstraint | undefined { + let valueType: Type | undefined; let type: Type | undefined; - if (constraint.value) { + if (constraint.valueType) { if ( - constraint.value.target.kind === "Model" && - isArrayModelType(program, constraint.value.target) + constraint.valueType.kind === "Model" && + isArrayModelType(program, constraint.valueType) ) { - value = { kind: "Value", target: constraint.value.target.indexer.value }; + valueType = constraint.valueType.indexer.value; } else { - return "unknown"; + return undefined; } } if (constraint.type) { if (constraint.type.kind === "Model" && isArrayModelType(program, constraint.type)) { type = constraint.type.indexer.value; } else { - return "unknown"; + return undefined; } } - return `(${getTSParmeterType({ + return { kind: "MixedConstraint", type, - value, - })})[]`; + valueType, + }; + } + function getRestTSParmeterType(constraint: MixedConstraint) { + const restItemConstraint = extractRestParamConstraint(constraint); + if (restItemConstraint === undefined) { + return "unknown"; + } + return `(${getTSParmeterType(restItemConstraint)})[]`; } function getTSParmeterType(constraint: MixedConstraint, isTarget?: boolean): string { - if (constraint.type && constraint.value) { - return `${getTypeConstraintTSType(constraint.type, isTarget)} | ${getValueTSType(constraint.value.target)}`; + if (constraint.type && constraint.valueType) { + return `${getTypeConstraintTSType(constraint.type, isTarget)} | ${getValueTSType(constraint.valueType)}`; } - if (constraint.value) { - return getValueTSType(constraint.value.target); + if (constraint.valueType) { + return getValueTSType(constraint.valueType); } else if (constraint.type) { return getTypeConstraintTSType(constraint.type, isTarget); } From 2678b38aea9e405831f0dcaa76faf2b31b7a00f3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 14:33:18 -0700 Subject: [PATCH 129/184] missing --- packages/compiler/src/server/type-signature.ts | 5 +---- packages/tspd/src/ref-doc/utils/type-signature.ts | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index 9f45288824..49ce850849 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -15,7 +15,6 @@ import { SyntaxKind, Type, UnionVariant, - ValueConstraint, } from "../core/types.js"; import { printId } from "../formatter/print/printer.js"; @@ -30,7 +29,7 @@ export function getSymbolSignature(program: Program, sym: Sym): string { return getTypeSignature(type); } -function getTypeSignature(type: Type | ValueConstraint): string { +function getTypeSignature(type: Type): string { switch (type.kind) { case "Scalar": case "Enum": @@ -47,8 +46,6 @@ function getTypeSignature(type: Type | ValueConstraint): string { return fence(getFunctionSignature(type)); case "Operation": return fence(getOperationSignature(type)); - case "Value": - return `valueof ${getTypeSignature(type)}`; case "String": // BUG: https://github.com/microsoft/typespec/issues/1350 - should escape string literal values return `(string)\n${fence(`"${type.value}"`)}`; diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 0501930ba9..43a6b01eaa 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -14,14 +14,10 @@ import { TemplateParameterDeclarationNode, Type, UnionVariant, - ValueConstraint, } from "@typespec/compiler"; /** @internal */ -export function getTypeSignature(type: Type | ValueConstraint): string { - if (type.kind === "Value") { - return `valueof ${getTypeSignature(type.target)}`; - } +export function getTypeSignature(type: Type): string { if (isReflectionType(type)) { return type.name; } From efb29df71a16898a7b03455a556bb89d75905d10 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 14:51:10 -0700 Subject: [PATCH 130/184] better way of dealing with templates --- packages/compiler/src/core/checker.ts | 47 +++++++++++---- .../src/core/helpers/string-template-utils.ts | 60 +++++++------------ packages/compiler/src/core/types.ts | 2 + 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 8266426d02..840e03e827 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -20,6 +20,7 @@ import { getTypeName, stringTemplateToString, } from "./helpers/index.js"; +import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { getMaxItems, getMaxLength, @@ -3129,26 +3130,49 @@ export function createChecker(program: Program): Checker { } return checkStringValue(createLiteralType(str), undefined, node); } else { + let hasNonStringElement = false; + let stringValue = node.head.value; + const spans: StringTemplateSpan[] = [createTemplateSpanLiteral(node.head)]; for (const [span, typeOrValue] of spanTypeOrValues) { - if (typeOrValue !== null) { - if (isValue(typeOrValue)) { - hasValue = true; - } else { - hasType = true; - spans.push(createTemplateSpanValue(span.expression, typeOrValue)); - } + compilerAssert(typeOrValue !== null && isType(typeOrValue), "Expected type."); + + const spanValue = createTemplateSpanValue(span.expression, typeOrValue); + spans.push(spanValue); + const spanValueAsString = stringifyTypeForTemplate(typeOrValue); + if (spanValueAsString) { + stringValue += spanValueAsString; + } else { + hasNonStringElement = true; } + spans.push(createTemplateSpanLiteral(span.literal)); + stringValue += span.literal.value; } return createType({ kind: "StringTemplate", node, spans, + stringValue: hasNonStringElement ? undefined : stringValue, }); } } + function stringifyTypeForTemplate(type: Type): string | undefined { + switch (type.kind) { + case "String": + case "Number": + case "Boolean": + return String(type.value); + case "StringTemplate": + if (type.stringValue !== undefined) { + return type.stringValue; + } + return undefined; + default: + return undefined; + } + } function stringifyValueForTemplate(value: Value): string { switch (value.valueKind) { case "StringValue": @@ -3681,9 +3705,12 @@ export function createChecker(program: Program): Checker { } let value: string; if (literalType.kind === "StringTemplate") { - const [result, diagnostics] = stringTemplateToString(literalType); - value = result; - reportCheckerDiagnostics(diagnostics); + if (literalType.stringValue) { + value = literalType.stringValue; + } else { + reportCheckerDiagnostics(explainStringTemplateNotSerializable(literalType)); + return null; + } } else { value = literalType.value; } diff --git a/packages/compiler/src/core/helpers/string-template-utils.ts b/packages/compiler/src/core/helpers/string-template-utils.ts index 23342e6e18..c9cbcc44c3 100644 --- a/packages/compiler/src/core/helpers/string-template-utils.ts +++ b/packages/compiler/src/core/helpers/string-template-utils.ts @@ -1,54 +1,36 @@ import { createDiagnosticCollector } from "../diagnostics.js"; import { createDiagnostic } from "../messages.js"; -import { Diagnostic, StringTemplate } from "../types.js"; -import { getTypeName } from "./type-name-utils.js"; +import type { Diagnostic, StringTemplate } from "../types.js"; /** - * Convert a string template to a string value. - * Only literal interpolated can be converted to string. - * Otherwise diagnostics will be reported. - * - * @param stringTemplate String template to convert. + * @deprecated use `{@link StringTemplate["stringValue"]} property on {@link StringTemplate} instead. */ export function stringTemplateToString( stringTemplate: StringTemplate ): [string, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - const result = stringTemplate.spans - .map((x) => { - if (x.isInterpolated) { - switch (x.type.kind) { - case "String": - case "Number": - case "Boolean": - return String(x.type.value); - case "StringTemplate": - return diagnostics.pipe(stringTemplateToString(x.type)); - case "TemplateParameter": - if (x.type.constraint && x.type.constraint.valueType !== undefined) { - return ""; - } - // eslint-disable-next-line no-fallthrough - default: - diagnostics.add( - createDiagnostic({ - code: "non-literal-string-template", - target: x.node, - }) - ); - return getTypeName(x.type); - } - } else { - return x.type.value; - } - }) - .join(""); - return diagnostics.wrap(result); + if (stringTemplate.stringValue !== undefined) { + return [stringTemplate.stringValue, []]; + } else { + return ["", explainStringTemplateNotSerializable(stringTemplate)]; + } } export function isStringTemplateSerializable( stringTemplate: StringTemplate ): [boolean, readonly Diagnostic[]] { + if (stringTemplate.stringValue !== undefined) { + return [true, []]; + } else { + return [false, explainStringTemplateNotSerializable(stringTemplate)]; + } +} + +/** + * get a list of diagnostic explaining why this string template cannot be converted to a string. + */ +export function explainStringTemplateNotSerializable( + stringTemplate: StringTemplate +): readonly Diagnostic[] { const diagnostics = createDiagnosticCollector(); for (const span of stringTemplate.spans) { if (span.isInterpolated) { @@ -75,5 +57,5 @@ export function isStringTemplateSerializable( } } } - return [diagnostics.diagnostics.length === 0, diagnostics.diagnostics]; + return diagnostics.diagnostics; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 908b1ca41b..3fc7b4cc5f 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -591,6 +591,8 @@ export interface BooleanLiteral extends BaseType { export interface StringTemplate extends BaseType { kind: "StringTemplate"; + /** If the template can be render as as string this is the string value */ + stringValue?: string; node: StringTemplateExpressionNode; spans: StringTemplateSpan[]; } From 1b5ea8e453e025e772caa107797a89f9f5da2cec Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 14:57:15 -0700 Subject: [PATCH 131/184] string template that serialize to string is assignable to the string literal --- packages/compiler/src/core/checker.ts | 8 +++++--- packages/compiler/test/checker/relation.test.ts | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 840e03e827..c95b1eee3c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -18,7 +18,6 @@ import { getLocationContext, getNamespaceFullName, getTypeName, - stringTemplateToString, } from "./helpers/index.js"; import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { @@ -7044,7 +7043,10 @@ export function createChecker(program: Program): Checker { return false; } if (target.kind === "String") { - return source.kind === "String" && target.value === source.value; + return ( + (source.kind === "String" && source.value === target.value) || + (source.kind === "StringTemplate" && source.stringValue === target.value) + ); } if (target.kind === "Number") { return source.kind === "Number" && target.value === source.value; @@ -7983,7 +7985,7 @@ function unsafe_projectionArgumentMarshalForJS(arg: Type): any { if (arg.kind === "Boolean" || arg.kind === "String" || arg.kind === "Number") { return arg.value; } else if (arg.kind === "StringTemplate") { - return stringTemplateToString(arg)[0]; + return arg.stringValue; } return arg as any; } diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index ba3e39403a..bf3faff5d9 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -370,6 +370,10 @@ describe("compiler: checker: type relations", () => { await expectTypeAssignable({ source: `"foo"`, target: `"foo"` }); }); + it("can assign equivalent string template", async () => { + await expectTypeAssignable({ source: `"foo \${123} bar"`, target: `"foo 123 bar"` }); + }); + it("emit diagnostic when passing other literal", async () => { await expectTypeNotAssignable( { source: `"bar"`, target: `"foo"` }, From 6e5fb91ded7dcec6869b2ae01df5eb21ce6f0cc1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 18:48:37 -0700 Subject: [PATCH 132/184] Move mixedconstraint to not be a type --- packages/compiler/src/core/checker.ts | 28 +++++++++---------- packages/compiler/src/core/diagnostics.ts | 2 +- .../src/core/helpers/type-name-utils.ts | 8 +++--- packages/compiler/src/core/program.ts | 2 +- packages/compiler/src/core/type-utils.ts | 2 +- packages/compiler/src/core/types.ts | 2 +- .../decorators-signatures.ts | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c95b1eee3c..38ae4eae7e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -913,10 +913,7 @@ export function createChecker(program: Program): Checker { function extractValueOfConstraints( constraint: CheckConstraint | undefined ): CheckValueConstraint | undefined { - if (constraint === undefined) { - return undefined; - } - if (constraint.constraint.valueType) { + if (constraint?.constraint.valueType) { return { kind: constraint.kind, type: constraint.constraint.valueType }; } else { return undefined; @@ -1749,7 +1746,7 @@ export function createChecker(program: Program): Checker { } } return { - kind: "MixedConstraint", + metaKind: "MixedConstraint", node, valueType: values.length === 0 @@ -2017,7 +2014,7 @@ export function createChecker(program: Program): Checker { if (mixed) { const type = node.type ? getParamConstraintEntityForNode(node.type) - : ({ kind: "MixedConstraint", type: unknownType } satisfies MixedConstraint); + : ({ metaKind: "MixedConstraint", type: unknownType } satisfies MixedConstraint); parameterType = createType({ ...base, type, @@ -2055,7 +2052,7 @@ export function createChecker(program: Program): Checker { default: const [kind, entity] = getTypeOrValueOfTypeForNode(node, mapper); return { - kind: "MixedConstraint", + metaKind: "MixedConstraint", node: node, type: kind === "value" ? undefined : entity, valueType: kind === "value" ? entity : undefined, @@ -4914,7 +4911,7 @@ export function createChecker(program: Program): Checker { } return { - kind: "MixedConstraint", + metaKind: "MixedConstraint", type, valueType, }; @@ -6785,10 +6782,10 @@ export function createChecker(program: Program): Checker { // BACKCOMPAT: Added May 2023 sprint, to be removed by June 2023 sprint if ( "kind" in source && - "kind" in target && + "metaKind" in target && source.kind === "TemplateParameter" && source.constraint?.type && - target.kind === "MixedConstraint" && + target.metaKind === "MixedConstraint" && target.valueType ) { const [assignable] = isTypeAssignableToInternal( @@ -6818,14 +6815,17 @@ export function createChecker(program: Program): Checker { if (isValue(target)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - if (target.kind === "MixedConstraint") { + if ("metaKind" in target) { return isAssignableToMixedConstraint(source, target, diagnosticTarget, relationCache); } - if (isValue(source) || (source.kind === "MixedConstraint" && source.valueType)) { + if ( + isValue(source) || + ("metaKind" in source && source.metaKind === "MixedConstraint" && source.valueType) + ) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - if (source.kind === "MixedConstraint") { + if ("metaKind" in source) { return isTypeAssignableToInternal(source.type!, target, diagnosticTarget, relationCache); } @@ -6926,7 +6926,7 @@ export function createChecker(program: Program): Checker { diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if ("kind" in source && source.kind === "MixedConstraint") { + if ("metaKind" in source && source.metaKind === "MixedConstraint") { if (source.type && target.type) { const [variantAssignable, diagnostics] = isTypeAssignableToInternal( source.type, diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index 98657cd2ce..ce722b15aa 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -83,7 +83,7 @@ export function getSourceLocation( return target; } - if (!("kind" in target) && !("valueKind" in target)) { + if (!("kind" in target) && !("valueKind" in target) && !("metaKind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { target = target.symbolSource!; diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 9fd73c6c94..f9053aaf9b 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -1,6 +1,6 @@ import { printId } from "../../formatter/print/printer.js"; import { isDefined } from "../../utils/misc.js"; -import { isTemplateInstance, isValue } from "../type-utils.js"; +import { isTemplateInstance, isType, isValue } from "../type-utils.js"; import type { Entity, Enum, @@ -86,8 +86,10 @@ function getValuePreview(value: Value, options?: TypeNameOptions): string { export function getEntityName(entity: Entity, options?: TypeNameOptions): string { if (isValue(entity)) { return getValuePreview(entity, options); + } else if (isType(entity)) { + return getTypeName(entity, options); } else { - switch (entity.kind) { + switch (entity.metaKind) { case "MixedConstraint": return [ entity.type && getEntityName(entity.type), @@ -95,8 +97,6 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string ] .filter(isDefined) .join(" | "); - default: - return getTypeName(entity, options); } } } diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 7f17b464e4..2666eb1972 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -1174,7 +1174,7 @@ export async function compile( } function getNode(target: Node | Entity | Sym): Node | undefined { - if (!("kind" in target) && !("valueKind" in target)) { + if (!("kind" in target) && !("valueKind" in target) && !("metaKind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { return target.symbolSource!.declarations[0]; diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index 3eb197109c..df9d87cb42 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -44,7 +44,7 @@ export function isNullType(type: Entity): type is NullType { } export function isType(entity: Entity): entity is Type { - return "kind" in entity && entity.kind !== "MixedConstraint"; + return "kind" in entity; } export function isValue(entity: Entity): entity is Value { return "valueKind" in entity; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 3fc7b4cc5f..e56343f03f 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -172,7 +172,7 @@ export interface Projector { } export interface MixedConstraint { - readonly kind: "MixedConstraint"; + readonly metaKind: "MixedConstraint"; readonly node?: UnionExpressionNode | Expression; /** Type constraints */ diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index f45fcbc7d8..41dcbd237c 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -137,7 +137,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } return { - kind: "MixedConstraint", + metaKind: "MixedConstraint", type, valueType, }; From 5b5f3bc56a2241994527815adb6947cb35ac34c7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Sat, 20 Apr 2024 19:04:41 -0700 Subject: [PATCH 133/184] Fix doc generation --- docs/standard-library/built-in-decorators.md | 54 +++++++++---------- .../tspd/src/ref-doc/emitters/markdown.ts | 24 ++++++--- packages/tspd/src/ref-doc/types.ts | 4 +- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md index fe3cf11a22..35d318e512 100644 --- a/docs/standard-library/built-in-decorators.md +++ b/docs/standard-library/built-in-decorators.md @@ -21,7 +21,7 @@ NOTE: This decorator **should not** be used, use the `#deprecated` directive ins #### Parameters | Name | Type | Description | |------|------|-------------| -| message | `valueof string` | Deprecation message. | +| message | [valueof `string`](#string) | Deprecation message. | #### Examples @@ -47,7 +47,7 @@ Specify the property to be used to discriminate this type. #### Parameters | Name | Type | Description | |------|------|-------------| -| propertyName | `valueof string` | The property name to use for discrimination | +| propertyName | [valueof `string`](#string) | The property name to use for discrimination | #### Examples @@ -82,7 +82,7 @@ Attach a documentation string. #### Parameters | Name | Type | Description | |------|------|-------------| -| doc | `valueof string` | Documentation string | +| doc | [valueof `string`](#string) | Documentation string | | formatArgs | `{}` | Record with key value pair that can be interpolated in the doc. | #### Examples @@ -142,8 +142,8 @@ Provide an alternative name for this type when serialized to the given mime type #### Parameters | Name | Type | Description | |------|------|-------------| -| mimeType | `valueof string` | Mime type this should apply to. The mime type should be a known mime type as described here https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types without any suffix (e.g. `+json`) | -| name | `valueof string` | Alternative name | +| mimeType | [valueof `string`](#string) | Mime type this should apply to. The mime type should be a known mime type as described here https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types without any suffix (e.g. `+json`) | +| name | [valueof `string`](#string) | Alternative name | #### Examples @@ -204,7 +204,7 @@ If an operation returns a union of success and errors it only describe the error #### Parameters | Name | Type | Description | |------|------|-------------| -| doc | `valueof string` | Documentation string | +| doc | [valueof `string`](#string) | Documentation string | #### Examples @@ -230,7 +230,7 @@ The format names are open ended and are left to emitter to interpret. #### Parameters | Name | Type | Description | |------|------|-------------| -| format | `valueof string` | format name. | +| format | [valueof `string`](#string) | format name. | #### Examples @@ -254,7 +254,7 @@ Specifies how a templated type should name their instances. #### Parameters | Name | Type | Description | |------|------|-------------| -| name | `valueof string` | name the template instance should take | +| name | [valueof `string`](#string) | name the template instance should take | | formatArgs | `unknown` | Model with key value used to interpolate the name | #### Examples @@ -282,7 +282,7 @@ A debugging decorator used to inspect a type. #### Parameters | Name | Type | Description | |------|------|-------------| -| text | `valueof string` | Custom text to log | +| text | [valueof `string`](#string) | Custom text to log | @@ -300,7 +300,7 @@ A debugging decorator used to inspect a type name. #### Parameters | Name | Type | Description | |------|------|-------------| -| text | `valueof string` | Custom text to log | +| text | [valueof `string`](#string) | Custom text to log | @@ -318,7 +318,7 @@ Mark a model property as the key to identify instances of that type #### Parameters | Name | Type | Description | |------|------|-------------| -| altName | `valueof string` | Name of the property. If not specified, the decorated property name is used. | +| altName | [valueof `string`](#string) | Name of the property. If not specified, the decorated property name is used. | #### Examples @@ -390,7 +390,7 @@ Specify the maximum number of items this array should have. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof integer` | Maximum number | +| value | [valueof `integer`](#integer) | Maximum number | #### Examples @@ -414,7 +414,7 @@ Specify the maximum length this string type should be. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof integer` | Maximum length | +| value | [valueof `integer`](#integer) | Maximum length | #### Examples @@ -438,7 +438,7 @@ Specify the maximum value this numeric type should be. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof numeric` | Maximum value | +| value | [valueof `numeric`](#numeric) | Maximum value | #### Examples @@ -463,7 +463,7 @@ value. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof numeric` | Maximum value | +| value | [valueof `numeric`](#numeric) | Maximum value | #### Examples @@ -487,7 +487,7 @@ Specify the minimum number of items this array should have. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof integer` | Minimum number | +| value | [valueof `integer`](#integer) | Minimum number | #### Examples @@ -511,7 +511,7 @@ Specify the minimum length this string type should be. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof integer` | Minimum length | +| value | [valueof `integer`](#integer) | Minimum length | #### Examples @@ -535,7 +535,7 @@ Specify the minimum value this numeric type should be. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof numeric` | Minimum value | +| value | [valueof `numeric`](#numeric) | Minimum value | #### Examples @@ -560,7 +560,7 @@ value. #### Parameters | Name | Type | Description | |------|------|-------------| -| value | `valueof numeric` | Minimum value | +| value | [valueof `numeric`](#numeric) | Minimum value | #### Examples @@ -636,8 +636,8 @@ validates a GUID string might have a message like "Must be a valid GUID." #### Parameters | Name | Type | Description | |------|------|-------------| -| pattern | `valueof string` | Regular expression. | -| validationMessage | `valueof string` | Optional validation message that may provide context when validation fails. | +| pattern | [valueof `string`](#string) | Regular expression. | +| validationMessage | [valueof `string`](#string) | Optional validation message that may provide context when validation fails. | #### Examples @@ -663,8 +663,8 @@ Provide an alternative name for this type. #### Parameters | Name | Type | Description | |------|------|-------------| -| targetName | `valueof string` | Projection target | -| projectedName | `valueof string` | Alternative name | +| targetName | [valueof `string`](#string) | Projection target | +| projectedName | [valueof `string`](#string) | Alternative name | #### Examples @@ -691,7 +691,7 @@ If an operation returns a union of success and errors it only describe the succe #### Parameters | Name | Type | Description | |------|------|-------------| -| doc | `valueof string` | Documentation string | +| doc | [valueof `string`](#string) | Documentation string | #### Examples @@ -793,7 +793,7 @@ Typically a short, single-line description. #### Parameters | Name | Type | Description | |------|------|-------------| -| summary | `valueof string` | Summary string. | +| summary | [valueof `string`](#string) | Summary string. | #### Examples @@ -817,7 +817,7 @@ Attaches a tag to an operation, interface, or namespace. Multiple `@tag` decorat #### Parameters | Name | Type | Description | |------|------|-------------| -| tag | `valueof string` | Tag value | +| tag | [valueof `string`](#string) | Tag value | @@ -879,7 +879,7 @@ Set the visibility of key properties in a model if not already set. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibility | `valueof string` | The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. | +| visibility | [valueof `string`](#string) | The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. | diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts index 955657c0a3..7b4c33ae2d 100644 --- a/packages/tspd/src/ref-doc/emitters/markdown.ts +++ b/packages/tspd/src/ref-doc/emitters/markdown.ts @@ -1,4 +1,4 @@ -import { Entity, getEntityName, isType, resolvePath } from "@typespec/compiler"; +import { Entity, MixedConstraint, getEntityName, isType, resolvePath } from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -187,23 +187,24 @@ export class MarkdownRenderer { return [base]; } - ref(type: Entity): string { + ref(type: Entity, prefix: string = ""): string { const namedType = isType(type) && this.refDoc.getNamedTypeRefDoc(type); if (namedType) { return link( - inlinecode(namedType.name), + prefix + inlinecode(namedType.name), `${this.filename(namedType)}#${this.anchorId(namedType)}` ); } // So we don't show (anonymous model) until this gets improved. if ("kind" in type && type.kind === "Model" && type.name === "" && type.properties.size > 0) { - return inlinecode("{...}"); + return inlinecode(prefix + "{...}"); } return inlinecode( - getEntityName(type, { - namespaceFilter: (ns) => !this.refDoc.namespaces.some((x) => x.name === ns.name), - }) + prefix + + getEntityName(type, { + namespaceFilter: (ns) => !this.refDoc.namespaces.some((x) => x.name === ns.name), + }) ); } @@ -260,7 +261,7 @@ export class MarkdownRenderer { if (dec.parameters.length > 0) { const paramTable: string[][] = [["Name", "Type", "Description"]]; for (const param of dec.parameters) { - paramTable.push([param.name, this.ref(param.type.type), param.doc]); + paramTable.push([param.name, this.mixedConstraint(param.type.type), param.doc]); } content.push(section("Parameters", [table(paramTable), ""])); } else { @@ -272,6 +273,13 @@ export class MarkdownRenderer { return section(this.headingTitle(dec), content); } + mixedConstraint(constraint: MixedConstraint): string { + return [ + ...(constraint.type ? [this.ref(constraint.type)] : []), + ...(constraint.valueType ? [this.ref(constraint.valueType, "valueof ")] : []), + ].join(" | "); + } + examples(examples: readonly ExampleRefDoc[]) { const content: MarkdownDoc = []; if (examples.length === 0) { diff --git a/packages/tspd/src/ref-doc/types.ts b/packages/tspd/src/ref-doc/types.ts index e39fe0b2ee..cb5cfec0fc 100644 --- a/packages/tspd/src/ref-doc/types.ts +++ b/packages/tspd/src/ref-doc/types.ts @@ -1,10 +1,10 @@ import { Decorator, Enum, - FunctionParameter, Interface, LinterRuleDefinition, LinterRuleSet, + MixedFunctionParameter, Model, ModelProperty, NodePackage, @@ -119,7 +119,7 @@ export type DecoratorRefDoc = NamedTypeRefDoc & { }; export type FunctionParameterRefDoc = { - readonly type: FunctionParameter; + readonly type: MixedFunctionParameter; readonly name: string; readonly doc: string; readonly optional: boolean; From 6ad1cd45ef6740ce6f23acff37c9f678aadaa5fb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 22 Apr 2024 14:38:35 -0700 Subject: [PATCH 134/184] Fix docs --- packages/website/sidebars.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/sidebars.ts b/packages/website/sidebars.ts index 54be7f5de8..8d16455be6 100644 --- a/packages/website/sidebars.ts +++ b/packages/website/sidebars.ts @@ -95,8 +95,8 @@ const sidebars: SidebarsConfig = { "language-basics/intersections", "language-basics/type-literals", "language-basics/aliases", + "language-basics/values", "language-basics/type-relations", - "language-basics/type-and-values", ], }, { From 3fa5a921b779b372debaaa83827b21c2515258c1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 22 Apr 2024 14:40:56 -0700 Subject: [PATCH 135/184] format --- docs/language-basics/values.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 7f5edd441e..ec45873f0a 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -14,10 +14,7 @@ There are three kinds of values: objects, arrays, and scalars. These values can Object values use the syntax `#{}` and can define any number of properties. For example: ```typespec -const point = #{ - x: 0, - y: 0 -} +const point = #{ x: 0, y: 0 }; ``` The object value's properties must refer to other values. It is an error to reference a type. @@ -35,10 +32,7 @@ const example = #{ Array values use the syntax `#[]` and can define any number of items. For example: ```typespec -const points = #[ - #{ x: 0, y: 0}, - #{ x: 1, y: 1} -] +const points = #[#{ x: 0, y: 0 }, #{ x: 1, y: 1 }]; ``` As with object values, array values cannot contain types. From 73e1c6cba8ff33b5d4194dc37370c1485bf3f05b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 08:18:36 -0700 Subject: [PATCH 136/184] Treat templates as syntax template (#8) --- packages/compiler/src/core/checker.ts | 370 +++++++++++++----- .../src/core/helpers/type-name-utils.ts | 2 + packages/compiler/src/core/projector.ts | 12 +- packages/compiler/src/core/type-utils.ts | 5 +- packages/compiler/src/core/types.ts | 22 +- .../compiler/test/checker/relation.test.ts | 39 +- .../compiler/test/checker/templates.test.ts | 16 +- .../test/checker/valueof-casting.test.ts | 47 +-- .../test/checker/values/array-values.test.ts | 23 +- .../test/checker/values/object-values.test.ts | 26 +- .../compiler/test/checker/values/utils.ts | 55 ++- .../projection/projector-identity.test.ts | 10 +- packages/protobuf/src/transform/index.ts | 8 +- packages/versioning/src/validate.ts | 4 +- 14 files changed, 420 insertions(+), 219 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 38ae4eae7e..f9e380111d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -52,6 +52,7 @@ import { isArrayModelType, isErrorType, isNeverType, + isNullType, isTemplateInstance, isType, isUnknownType, @@ -94,6 +95,7 @@ import { FunctionType, IdentifierKind, IdentifierNode, + IndeterminateEntity, Interface, InterfaceStatementNode, IntersectionExpressionNode, @@ -188,6 +190,7 @@ import { Type, TypeInstantiationMap, TypeMapper, + TypeOfExpressionNode, TypeOrReturnRecord, TypeReferenceNode, TypeSpecScriptNode, @@ -687,11 +690,14 @@ export function createChecker(program: Program): Checker { } function getTypeForNode(node: Node, mapper?: TypeMapper): Type { - const typeOrValue = getTypeOrValueForNode(node, mapper, undefined); - if (typeOrValue === null) { + const entity = getTypeOrValueOrIndeterminateForNode(node, mapper); + if (entity === null) { return errorType; } - if (isValue(typeOrValue)) { + if ("metaKind" in entity) { + return entity.type; + } + if (isValue(entity)) { reportCheckerDiagnostic( createDiagnostic({ code: "value-in-type", @@ -700,7 +706,7 @@ export function createChecker(program: Program): Checker { ); return errorType; } - return typeOrValue; + return entity; } function getValueForNode( @@ -709,11 +715,36 @@ export function createChecker(program: Program): Checker { constraint?: CheckValueConstraint, options: { legacyTupleAndModelCast?: boolean } = {} ): Value | null { - let entity = getTypeOrValueForNodeInternal(node, mapper, constraint); - if (entity === null || isValue(entity)) { + const initial = getTypeOrValueForNodeInternal(node, mapper, constraint); + if (initial === null) { + return null; + } + let entity: Type | Value | null; + if ("metaKind" in initial) { + compilerAssert(initial.metaKind === "Indeterminate", "Expected indeterminate entity"); + entity = tryUsingValueOfType(initial.type, constraint, node); + if (options.legacyTupleAndModelCast && entity !== null && isType(entity)) { + entity = legacy_tryTypeToValueCast(entity, constraint, node); + } + } else { + entity = initial; + } + if (entity === null) { + return null; + } + if (isValue(entity)) { return entity; } - entity = tryUsingValueOfType(entity, constraint, node, options); + if (isType(entity)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity) }, + target: node, + }) + ); + return null; + } if (entity === null || isValue(entity)) { return entity; } @@ -731,8 +762,7 @@ export function createChecker(program: Program): Checker { function tryUsingValueOfType( type: Type, constraint: CheckValueConstraint | undefined, - node: Node, - options: { legacyTupleAndModelCast?: boolean } = {} + node: Node ): Type | Value | null { switch (type.kind) { case "String": @@ -746,14 +776,6 @@ export function createChecker(program: Program): Checker { return checkEnumValue(type, constraint, node); case "UnionVariant": return tryUsingValueOfType(type.type, constraint, node); - case "Tuple": - return options.legacyTupleAndModelCast - ? legacy_tryUsingTupleAsArrayValue(type, constraint?.type, node) - : type; - case "Model": - return options.legacyTupleAndModelCast - ? legacy_tryUsingModelAsObjectValue(type, constraint?.type, node) - : type; case "Intrinsic": switch (type.name) { case "null": @@ -765,6 +787,21 @@ export function createChecker(program: Program): Checker { } } + function legacy_tryTypeToValueCast( + type: Type, + constraint: CheckValueConstraint | undefined, + node: Node + ): Type | Value | null { + switch (type.kind) { + case "Tuple": + return legacy_tryUsingTupleAsArrayValue(type, constraint?.type, node); + case "Model": + return legacy_tryUsingModelAsObjectValue(type, constraint?.type, node); + default: + return type; + } + } + // Legacy behavior to smooth transition to object values. function legacy_tryUsingModelAsObjectValue( model: Model, @@ -795,12 +832,14 @@ export function createChecker(program: Program): Checker { }; for (const prop of model.properties.values()) { - const propValue = tryUsingValueOfType( - prop.type, - { kind: "assignment", type: prop.type }, - node, - { legacyTupleAndModelCast: true } - ); + let propValue = tryUsingValueOfType(prop.type, { kind: "assignment", type: prop.type }, node); + if (propValue !== null && isType(propValue)) { + propValue = legacy_tryTypeToValueCast( + propValue, + { kind: "assignment", type: prop.type }, + node + ); + } if (propValue == null) { return null; } else if (!isValue(propValue)) { @@ -849,14 +888,18 @@ export function createChecker(program: Program): Checker { : type?.kind === "Tuple" ? type.values[index] : undefined; - const value = tryUsingValueOfType( + let value = tryUsingValueOfType( item, itemType && { kind: "assignment", type: itemType }, - node, - { - legacyTupleAndModelCast: true, - } + node ); + if (value !== null && isType(value)) { + value = legacy_tryTypeToValueCast( + value, + itemType && { kind: "assignment", type: itemType }, + node + ); + } if (value === null) { return null; } else if (!isValue(value)) { @@ -897,15 +940,36 @@ export function createChecker(program: Program): Checker { ): Type | Value | null { const valueConstraint = extractValueOfConstraints(constraint); const entity = getTypeOrValueForNodeInternal(node, mapper, valueConstraint); - if (entity === null || isValue(entity)) { + if (entity === null) { + return entity; + } else if (isType(entity)) { + if (valueConstraint) { + return legacy_tryTypeToValueCast(entity, valueConstraint, node); + } else { + return entity; + } + } else if (isValue(entity)) { return entity; } + compilerAssert(entity.metaKind === "Indeterminate", "Expected indeterminate entity"); if (valueConstraint) { - return tryUsingValueOfType(entity, valueConstraint, node, { legacyTupleAndModelCast: true }); + return tryUsingValueOfType(entity.type, valueConstraint, node); } - return entity; + return entity.type; + } + + /** + * Gets a type or value depending on the node and current constraint. + * For nodes that can be both type or values(e.g. string), the value will be returned if the constraint expect a value of that type even if the constrain also allows the type. + * This means that if the constraint is `string | valueof string` passing `"abc"` will send the value `"abc"` and not the type `"abc"`. + */ + function getTypeOrValueOrIndeterminateForNode( + node: Node, + mapper?: TypeMapper + ): Type | Value | IndeterminateEntity | null { + return getTypeOrValueForNodeInternal(node, mapper); } // TODO: do we still need this? @@ -925,7 +989,7 @@ export function createChecker(program: Program): Checker { node: Node, mapper?: TypeMapper, valueConstraint?: CheckValueConstraint | undefined - ): Type | Value | null { + ): Type | Value | IndeterminateEntity | null { switch (node.kind) { case SyntaxKind.ModelExpression: return checkModel(node, mapper); @@ -994,6 +1058,8 @@ export function createChecker(program: Program): Checker { return checkConst(node); case SyntaxKind.CallExpression: return checkCallExpression(node, mapper); + case SyntaxKind.TypeOfExpression: + return checkTypeOfExpression(node, mapper); default: return errorType; } @@ -1045,19 +1111,19 @@ export function createChecker(program: Program): Checker { function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: undefined - ): TemplateParameter; + ): TemplateParameter | IndeterminateEntity; function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: TypeMapper - ): Type | Value; + ): Type | Value | IndeterminateEntity; function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: TypeMapper | undefined - ): Type | Value; + ): Type | Value | IndeterminateEntity; function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: TypeMapper | undefined - ): Type | Value { + ): Type | Value | IndeterminateEntity { const parentNode = node.parent!; const grandParentNode = parentNode.parent; const links = getSymbolLinks(node.symbol); @@ -1132,9 +1198,9 @@ export function createChecker(program: Program): Checker { templateParameters: readonly TemplateParameterDeclarationNode[], index: number, constraint: Entity | undefined - ): Type | Value { + ): Type | Value | IndeterminateEntity { function visit(node: Node) { - const type = getTypeOrValueForNode(node); + const type = getTypeOrValueOrIndeterminateForNode(node); let hasError = false; if (type !== null && "kind" in type && type.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { @@ -1197,7 +1263,7 @@ export function createChecker(program: Program): Checker { node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplate = true - ): Type | Value { + ): Type | Value | IndeterminateEntity { const sym = resolveTypeReferenceSym(node, mapper); if (!sym) { return errorType; @@ -1209,8 +1275,8 @@ export function createChecker(program: Program): Checker { function checkTemplateArgument( node: TemplateArgumentNode, mapper: TypeMapper | undefined - ): Type | Value | null { - return getTypeOrValueForNode(node.argument, mapper); + ): Type | Value | IndeterminateEntity | null { + return getTypeOrValueOrIndeterminateForNode(node.argument, mapper); } function resolveTypeReference( @@ -1293,13 +1359,13 @@ export function createChecker(program: Program): Checker { args: readonly TemplateArgumentNode[], decls: readonly TemplateParameterDeclarationNode[], mapper: TypeMapper | undefined - ): Map { + ): Map { const params = new Map(); const positional: TemplateParameter[] = []; interface TemplateParameterInit { decl: TemplateParameterDeclarationNode; // Deferred initializer so that we evaluate the param arguments in definition order. - checkArgument: (() => [Node, Type | Value]) | null; + checkArgument: (() => [Node, Type | Value | IndeterminateEntity | null]) | null; } const initMap = new Map( decls.map((decl) => { @@ -1321,15 +1387,8 @@ export function createChecker(program: Program): Checker { let named = false; for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { - function deferredCheck(param: TemplateParameter): () => [Node, Type | Value] { - return () => [ - arg, - getTypeOrValueForNode( - arg.argument, - mapper, - param.constraint && { kind: "argument", constraint: param.constraint } - ) ?? errorType, - ]; + function deferredCheck(): [Node, Type | Value | IndeterminateEntity | null] { + return [arg, getTypeOrValueOrIndeterminateForNode(arg.argument, mapper)]; } if (arg.name) { @@ -1365,7 +1424,7 @@ export function createChecker(program: Program): Checker { continue; } - initMap.get(param)!.checkArgument = deferredCheck(param); + initMap.get(param)!.checkArgument = deferredCheck; } else { if (named) { reportCheckerDiagnostic( @@ -1391,15 +1450,18 @@ export function createChecker(program: Program): Checker { const param = positional[idx]; - initMap.get(param)!.checkArgument ??= deferredCheck(param); + initMap.get(param)!.checkArgument ??= deferredCheck; } } - const finalMap = initMap as unknown as Map; + const finalMap = initMap as unknown as Map< + TemplateParameter, + Type | Value | IndeterminateEntity + >; const mapperParams: TemplateParameter[] = []; - const mapperArgs: (Type | Value)[] = []; + const mapperArgs: (Type | Value | IndeterminateEntity)[] = []; for (const [param, { decl, checkArgument: init }] of [...initMap]) { - function commit(param: TemplateParameter, type: Type | Value): void { + function commit(param: TemplateParameter, type: Type | Value | IndeterminateEntity): void { finalMap.set(param, type); mapperParams.push(param); mapperArgs.push(type); @@ -1430,20 +1492,37 @@ export function createChecker(program: Program): Checker { } const [argNode, type] = init(); - + if (type === null) { + commit(param, unknownType); + continue; + } if (param.constraint) { const constraint = param.constraint.type?.kind === "TemplateParameter" ? finalMap.get(param.constraint.type)! : param.constraint; - if (isErrorType(type) || !checkTypeAssignable(type, constraint, argNode)) { - // TODO-TIM check if we expose this below + if (isType(type) && param.constraint?.valueType) { + const converted = legacy_tryTypeToValueCast( + type, + { kind: "argument", type: param.constraint.valueType }, + argNode + ); + // If we manage to convert it means this might be convertable so we skip type checking. + // However we still return the original entity + if (converted !== type) { + commit(param, type); + continue; + } + } + + if (param.constraint && !checkArgumentAssignable(type, constraint, argNode)) { const effectiveType = param.constraint.type ?? unknownType; + commit(param, effectiveType); continue; } - } else if ("kind" in type && isErrorType(type)) { + } else if (isErrorType(type)) { // If we got an error type we don't want to keep passing it through so we reduce to unknown // Similar to the above where if the type is not assignable to the constraint we reduce to the constraint commit(param, unknownType); @@ -1474,6 +1553,9 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); return errorType; } + if ("metaKind" in result) { + return result.type; + } return result; } @@ -1482,7 +1564,7 @@ export function createChecker(program: Program): Checker { node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplates = true - ): Type | Value | null { + ): Type | Value | IndeterminateEntity | null { if (sym.flags & SymbolFlags.Const) { return getValueForNode(sym.declarations[0], mapper); } @@ -1505,7 +1587,7 @@ export function createChecker(program: Program): Checker { const argumentNodes = node.kind === SyntaxKind.TypeReference ? node.arguments : []; const symbolLinks = getSymbolLinks(sym); - let baseType; + let baseType: Type; if ( sym.flags & (SymbolFlags.Model | @@ -1603,6 +1685,15 @@ export function createChecker(program: Program): Checker { } } + // Elements that could be used as type or values depending on the context + if ( + baseType.kind === "EnumMember" || + baseType.kind === "UnionVariant" || + isNullType(baseType) + ) { + return createIndeterminateEntity(baseType); + } + return baseType; } @@ -1646,7 +1737,7 @@ export function createChecker(program: Program): Checker { sym: Sym, node: TemplateableNode, mapper: TypeMapper | undefined - ): Type | Value { + ): Type { const type = sym.flags & SymbolFlags.Model ? checkModelStatement(node as ModelStatementNode, mapper) @@ -1666,7 +1757,7 @@ export function createChecker(program: Program): Checker { function getOrInstantiateTemplate( templateNode: TemplateableNode, params: TemplateParameter[], - args: (Type | Value)[], + args: (Type | Value | IndeterminateEntity)[], parentMapper: TypeMapper | undefined, instantiateTempalates = true ): Type { @@ -3091,11 +3182,11 @@ export function createChecker(program: Program): Checker { function checkStringTemplateExpresion( node: StringTemplateExpressionNode, mapper: TypeMapper | undefined - ): StringTemplate | StringValue | null { + ): IndeterminateEntity | StringValue | null { let hasType = false; let hasValue = false; const spanTypeOrValues = node.spans.map( - (span) => [span, getTypeOrValueForNode(span.expression, mapper)] as const + (span) => [span, getTypeOrValueOrIndeterminateForNode(span.expression, mapper)] as const ); for (const [_, typeOrValue] of spanTypeOrValues) { if (typeOrValue !== null) { @@ -3132,11 +3223,12 @@ export function createChecker(program: Program): Checker { const spans: StringTemplateSpan[] = [createTemplateSpanLiteral(node.head)]; for (const [span, typeOrValue] of spanTypeOrValues) { - compilerAssert(typeOrValue !== null && isType(typeOrValue), "Expected type."); + compilerAssert(typeOrValue !== null && !isValue(typeOrValue), "Expected type."); - const spanValue = createTemplateSpanValue(span.expression, typeOrValue); + const type = "metaKind" in typeOrValue ? typeOrValue.type : typeOrValue; + const spanValue = createTemplateSpanValue(span.expression, type); spans.push(spanValue); - const spanValueAsString = stringifyTypeForTemplate(typeOrValue); + const spanValueAsString = stringifyTypeForTemplate(type); if (spanValueAsString) { stringValue += spanValueAsString; } else { @@ -3146,14 +3238,23 @@ export function createChecker(program: Program): Checker { spans.push(createTemplateSpanLiteral(span.literal)); stringValue += span.literal.value; } - return createType({ - kind: "StringTemplate", - node, - spans, - stringValue: hasNonStringElement ? undefined : stringValue, - }); + return createIndeterminateEntity( + createType({ + kind: "StringTemplate", + node, + spans, + stringValue: hasNonStringElement ? undefined : stringValue, + }) + ); } } + + function createIndeterminateEntity(type: Type): IndeterminateEntity { + return { + metaKind: "Indeterminate", + type, + }; + } function stringifyTypeForTemplate(type: Type): string | undefined { switch (type.kind) { case "String": @@ -3206,16 +3307,25 @@ export function createChecker(program: Program): Checker { }); } - function checkStringLiteral(str: StringLiteralNode): StringLiteral { - return getLiteralType(str); + function checkStringLiteral(str: StringLiteralNode): IndeterminateEntity { + return { + metaKind: "Indeterminate", + type: getLiteralType(str), + }; } - function checkNumericLiteral(num: NumericLiteralNode): NumericLiteral { - return getLiteralType(num); + function checkNumericLiteral(num: NumericLiteralNode): IndeterminateEntity { + return { + metaKind: "Indeterminate", + type: getLiteralType(num), + }; } - function checkBooleanLiteral(bool: BooleanLiteralNode): BooleanLiteral { - return getLiteralType(bool); + function checkBooleanLiteral(bool: BooleanLiteralNode): IndeterminateEntity { + return { + metaKind: "Indeterminate", + type: getLiteralType(bool), + }; } function checkProgram() { @@ -3271,7 +3381,7 @@ export function createChecker(program: Program): Checker { function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { - getTypeOrValueForNode(statement, undefined); + getTypeOrValueOrIndeterminateForNode(statement, undefined); } } @@ -3397,7 +3507,9 @@ export function createChecker(program: Program): Checker { } // Some of the mapper args are still template parameter so we shouldn't create the type. - return mapper.args.every((t) => isValue(t) || t.kind !== "TemplateParameter"); + return mapper.args.every( + (t) => isValue(t) || "metaKind" in t || t.kind !== "TemplateParameter" + ); } function checkModelExpression(node: ModelExpressionNode, mapper: TypeMapper | undefined) { @@ -3981,6 +4093,23 @@ export function createChecker(program: Program): Checker { } } + function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { + const entity = getTypeOrValueForNodeInternal(node.target, mapper, undefined); + if (entity === null) { + return errorType; // TODO: emit error + } + if ("metaKind" in entity) { + return entity.type; + } + if (isType(entity)) { + return entity; // TODO: emit error + } + if (entity === null) { + return errorType; // TODO: emit error + } + return entity.type; + } + function createUnion(options: Type[]): Union { const variants = createRekeyableMap(); const union: Union = createAndFinishType({ @@ -4655,10 +4784,13 @@ export function createChecker(program: Program): Checker { * We do do checking here we just keep existing behavior. */ function checkLegacyDefault(defaultNode: Node): Type | undefined { - const resolved = getTypeOrValueForNode(defaultNode, undefined); - if (resolved === null || !isType(resolved)) { + const resolved = getTypeOrValueOrIndeterminateForNode(defaultNode, undefined); + if (resolved === null || isValue(resolved)) { return undefined; } + if ("metaKind" in resolved) { + return resolved.type; + } return resolved; } @@ -4777,7 +4909,10 @@ export function createChecker(program: Program): Checker { return [ false, node.arguments.map((argNode): DecoratorArgument => { - const type = getTypeOrValueForNode(argNode, mapper) ?? errorType; + let type = getTypeOrValueOrIndeterminateForNode(argNode, mapper) ?? errorType; + if ("metaKind" in type) { + type = type.type; + } return { value: type, jsValue: type, @@ -4835,6 +4970,7 @@ export function createChecker(program: Program): Checker { kind: "argument", constraint: perParamType, }); + if ( arg !== null && !(isType(arg) && isErrorType(arg)) && @@ -4939,7 +5075,7 @@ export function createChecker(program: Program): Checker { } function checkArgumentAssignable( - argumentType: Type | Value, + argumentType: Type | Value | IndeterminateEntity, parameterType: Entity, diagnosticTarget: DiagnosticTarget ): boolean { @@ -6690,7 +6826,7 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. */ function checkTypeAssignable( - source: Entity, + source: Entity | IndeterminateEntity, target: Entity, diagnosticTarget: DiagnosticTarget ): boolean { @@ -6720,7 +6856,7 @@ export function createChecker(program: Program): Checker { * @param diagnosticTarget Target for the diagnostic, unless something better can be inferred. */ function isTypeAssignableTo( - source: Entity, + source: Entity | IndeterminateEntity, target: Entity, diagnosticTarget: DiagnosticTarget ): [boolean, readonly Diagnostic[]] { @@ -6754,10 +6890,10 @@ export function createChecker(program: Program): Checker { } function isTypeAssignableToInternal( - source: Entity, + source: Entity | IndeterminateEntity, target: Entity, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Entity, Entity], Related> + relationCache: MultiKeyMap<[Entity | IndeterminateEntity, Entity], Related> ): [Related, readonly Diagnostic[]] { const cached = relationCache.get([source, target]); if (cached !== undefined) { @@ -6774,12 +6910,12 @@ export function createChecker(program: Program): Checker { } function isTypeAssignableToWorker( - source: Entity, + source: Entity | IndeterminateEntity, target: Entity, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - // BACKCOMPAT: Added May 2023 sprint, to be removed by June 2023 sprint + // BACKCOMPAT: Allow certain type to be accepted as values if ( "kind" in source && "metaKind" in target && @@ -6810,11 +6946,19 @@ export function createChecker(program: Program): Checker { if ("kind" in source && source.kind === "TemplateParameter") { source = source.constraint ?? unknownType; } + if ("metaKind" in target && target.metaKind === "Indeterminate") { + target = target.type; + } if (source === target) return [Related.true, []]; + if (isValue(target)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } + if ("metaKind" in source && source.metaKind === "Indeterminate") { + return isIndeterminateEntityAssignableTo(source, target, diagnosticTarget, relationCache); + } + if ("metaKind" in target) { return isAssignableToMixedConstraint(source, target, diagnosticTarget, relationCache); } @@ -6907,6 +7051,38 @@ export function createChecker(program: Program): Checker { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } + function isIndeterminateEntityAssignableTo( + indeterminate: IndeterminateEntity, + target: Type | MixedConstraint, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Entity, Entity], Related> + ): [Related, readonly Diagnostic[]] { + const [typeRelated, typeDiagnostics] = isTypeAssignableToInternal( + indeterminate.type, + target, + diagnosticTarget, + relationCache + ); + if (typeRelated) { + return [Related.true, []]; + } + + if ("metaKind" in target && target.valueType) { + const [valueRelated] = isTypeAssignableToInternal( + indeterminate.type, + target.valueType, + diagnosticTarget, + relationCache + ); + + if (valueRelated) { + return [Related.true, []]; + } + } + + return [Related.false, typeDiagnostics]; + } + function isAssignableToValueType( source: Entity, target: Type, @@ -7494,10 +7670,12 @@ function addDerivedModels(models: Set, possiblyDerivedModels: ReadonlySet function createTypeMapper( parameters: TemplateParameter[], - args: (Type | Value)[], + args: (Type | Value | IndeterminateEntity)[], parentMapper?: TypeMapper ): TypeMapper { - const map = new Map(parentMapper?.map ?? []); + const map = new Map( + parentMapper?.map ?? [] + ); for (const [index, param] of parameters.entries()) { map.set(param, args[index]); diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index f9053aaf9b..0e8fbb688c 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -97,6 +97,8 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string ] .filter(isDefined) .join(" | "); + case "Indeterminate": + return getTypeName(entity.type, options); } } } diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index 6d4a321713..907b99e4bd 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -8,6 +8,7 @@ import { DecoratorArgument, Enum, EnumMember, + IndeterminateEntity, Interface, Model, ModelProperty, @@ -97,11 +98,20 @@ export function createProjector( function projectType(type: Type): Type; function projectType(type: Value): Value; + function projectType(type: IndeterminateEntity): IndeterminateEntity; function projectType(type: Type | Value): Type | Value; - function projectType(type: Type | Value): Type | Value { + function projectType( + type: Type | Value | IndeterminateEntity + ): Type | Value | IndeterminateEntity; + function projectType( + type: Type | Value | IndeterminateEntity + ): Type | Value | IndeterminateEntity { if (isValue(type)) { return type; } + if ("metaKind" in type) { + return { metaKind: "Indeterminate", type: projectType(type.type) }; + } if (projectedTypes.has(type)) { return projectedTypes.get(type)!; } diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index df9d87cb42..de3b793a37 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -4,6 +4,7 @@ import { Entity, Enum, ErrorType, + IndeterminateEntity, Interface, Model, Namespace, @@ -43,10 +44,10 @@ export function isNullType(type: Entity): type is NullType { return "kind" in type && type.kind === "Intrinsic" && type.name === "null"; } -export function isType(entity: Entity): entity is Type { +export function isType(entity: Entity | IndeterminateEntity): entity is Type { return "kind" in entity; } -export function isValue(entity: Entity): entity is Value { +export function isValue(entity: Entity | IndeterminateEntity): entity is Value { return "valueKind" in entity; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index e56343f03f..384a707d71 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -86,9 +86,9 @@ export type TemplatedType = Model | Operation | Interface | Union | Scalar; export interface TypeMapper { partial: boolean; - getMappedType(type: TemplateParameter): Type | Value; - args: readonly (Type | Value)[]; - /** @internal */ map: Map; + getMappedType(type: TemplateParameter): Type | Value | IndeterminateEntity; + args: readonly (Type | Value | IndeterminateEntity)[]; + /** @internal */ map: Map; } export interface TemplatedTypeBase { @@ -96,7 +96,7 @@ export interface TemplatedTypeBase { /** * @deprecated use templateMapper instead. */ - templateArguments?: (Type | Value)[]; + templateArguments?: (Type | Value | IndeterminateEntity)[]; templateNode?: Node; } @@ -106,7 +106,7 @@ export interface TemplatedTypeBase { * - Values * - Value Constraints */ -export type Entity = Type | Value | MixedConstraint; +export type Entity = Type | Value | MixedConstraint | IndeterminateEntity; export type Type = | BooleanLiteral @@ -182,6 +182,12 @@ export interface MixedConstraint { readonly valueType?: Type; } +/** When an entity that could be used as a type or value has not figured out if it is a value or type yet. */ +export interface IndeterminateEntity { + readonly metaKind: "Indeterminate"; + readonly type: Type; // TODO: better typeing? +} + export interface IntrinsicType extends BaseType { kind: "Intrinsic"; name: "ErrorType" | "void" | "never" | "unknown" | "null"; @@ -657,7 +663,7 @@ export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; constraint?: MixedConstraint; - default?: Type | Value; + default?: Type | Value | IndeterminateEntity; } export interface Decorator extends BaseType { @@ -828,8 +834,8 @@ export const enum SymbolFlags { * Maps type arguments to instantiated type. */ export interface TypeInstantiationMap { - get(args: readonly (Type | Value)[]): Type | undefined; - set(args: readonly (Type | Value)[], type: Type): void; + get(args: readonly (Type | Value | IndeterminateEntity)[]): Type | undefined; + set(args: readonly (Type | Value | IndeterminateEntity)[], type: Type): void; } /** diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index bf3faff5d9..0165e2e0a8 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1192,8 +1192,8 @@ describe("compiler: checker: type relations", () => { `); expectDiagnostics(diagnostics, { - code: "missing-property", - message: `Property 'a' is missing on type '{ b: string }' but required in '{ a: string }'`, + code: "invalid-argument", + message: `Argument of type '{ b: string }' is not assignable to parameter of type '{ a: string }'`, }); }); @@ -1275,7 +1275,8 @@ describe("compiler: checker: type relations", () => { { source: `123`, target: "valueof string" }, { code: "invalid-argument", - message: "Argument of type '123' is not assignable to parameter of type 'string'", + message: + "Argument of type '123' is not assignable to parameter of type 'valueof string'", } ); }); @@ -1301,7 +1302,8 @@ describe("compiler: checker: type relations", () => { { source: `123`, target: "valueof boolean" }, { code: "invalid-argument", - message: "Argument of type '123' is not assignable to parameter of type 'boolean'", + message: + "Argument of type '123' is not assignable to parameter of type 'valueof boolean'", } ); }); @@ -1331,7 +1333,8 @@ describe("compiler: checker: type relations", () => { { source: `123456`, target: "valueof int16" }, { code: "invalid-argument", - message: "Argument of type '123456' is not assignable to parameter of type 'int16'", + message: + "Argument of type '123456' is not assignable to parameter of type 'valueof int16'", } ); }); @@ -1341,7 +1344,8 @@ describe("compiler: checker: type relations", () => { { source: `12.6`, target: "valueof int16" }, { code: "invalid-argument", - message: "Argument of type '12.6' is not assignable to parameter of type 'int16'", + message: + "Argument of type '12.6' is not assignable to parameter of type 'valueof int16'", } ); }); @@ -1351,7 +1355,7 @@ describe("compiler: checker: type relations", () => { { source: `"foo bar"`, target: "valueof int16" }, { code: "invalid-argument", - message: `Argument of type '"foo bar"' is not assignable to parameter of type 'int16'`, + message: `Argument of type '"foo bar"' is not assignable to parameter of type 'valueof int16'`, } ); }); @@ -1377,7 +1381,7 @@ describe("compiler: checker: type relations", () => { { source: `"foo bar"`, target: "valueof float32" }, { code: "invalid-argument", - message: `Argument of type '"foo bar"' is not assignable to parameter of type 'float32'`, + message: `Argument of type '"foo bar"' is not assignable to parameter of type 'valueof float32'`, } ); }); @@ -1443,7 +1447,7 @@ describe("compiler: checker: type relations", () => { }, { code: "invalid-argument", - message: `Argument of type '{ name: "foo", notDefined: "bar" }' is not assignable to parameter of type 'Info'`, + message: `Argument of type '#{name: "foo", notDefined: "bar"}' is not assignable to parameter of type 'valueof Info'`, } ); }); @@ -1469,7 +1473,7 @@ describe("compiler: checker: type relations", () => { }, { code: "invalid-argument", - message: `Argument of type '["foo"]' is not assignable to parameter of type 'Info'`, + message: `Argument of type '#["foo"]' is not assignable to parameter of type 'valueof Info'`, } ); }); @@ -1523,7 +1527,7 @@ describe("compiler: checker: type relations", () => { }, { code: "invalid-argument", - message: `Argument of type '{ name: "foo" }' is not assignable to parameter of type 'string[]'`, + message: `Argument of type '#{name: "foo"}' is not assignable to parameter of type 'valueof string[]'`, } ); }); @@ -1555,7 +1559,7 @@ describe("compiler: checker: type relations", () => { }, { code: "invalid-argument", - message: `Argument of type '["foo"]' is not assignable to parameter of type '[string, string]'`, + message: `Argument of type '#["foo"]' is not assignable to parameter of type 'valueof [string, string]'`, } ); }); @@ -1568,7 +1572,7 @@ describe("compiler: checker: type relations", () => { }, { code: "invalid-argument", - message: `Argument of type '["a", "b", "c"]' is not assignable to parameter of type '[string, string]'`, + message: `Argument of type '#["a", "b", "c"]' is not assignable to parameter of type 'valueof [string, string]'`, } ); }); @@ -1604,8 +1608,8 @@ describe("compiler: checker: type relations", () => { model B is A<┆T> {}`); const diagnostics = await runner.diagnose(source); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type 'valueof unknown' is not assignable to type 'unknown'", + code: "invalid-argument", + message: "Argument of type 'T' is not assignable to parameter of type 'unknown'", pos, }); }); @@ -1634,7 +1638,10 @@ describe("compiler: checker: type relations", () => { ["#[]", "unknown[]"], ["#[]", "unknown"], ])(`%s => %s`, async (source, target) => { - await expectValueNotAssignableToConstraint({ source, target }, { code: "unassignable" }); + await expectValueNotAssignableToConstraint( + { source, target }, + { code: "invalid-argument" } + ); }); }); diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 1b5ffc97d7..cef1cf0abc 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -239,8 +239,8 @@ describe("compiler: templates", () => { const diagnostics = await testHost.diagnose("main.tsp"); // Only one error, Bar can't be created as T is not constraint to object expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type 'unknown' is not assignable to type '{}'", + code: "invalid-argument", + message: "Argument of type 'T' is not assignable to parameter of type '{}'", pos, }); }); @@ -279,8 +279,8 @@ describe("compiler: templates", () => { const diagnostics = await testHost.diagnose("main.tsp"); // Only one error, Bar can't be created as T is not constraint to object expectDiagnostics(diagnostics, { - code: "unassignable", - message: `Type '"abc"' is not assignable to type '{}'`, + code: "invalid-argument", + message: `Argument of type '"abc"' is not assignable to parameter of type '{}'`, pos, }); }); @@ -481,8 +481,8 @@ describe("compiler: templates", () => { `); const diagnostics = await runner.diagnose(source); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '456' is not assignable to type 'string'", + code: "invalid-argument", + message: "Argument of type '456' is not assignable to parameter of type 'string'", pos, end, }); @@ -508,8 +508,8 @@ describe("compiler: templates", () => { `); const diagnostics = await runner.diagnose(source); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type 'unknown' is not assignable to type 'string'", + code: "invalid-argument", + message: "Argument of type 'T' is not assignable to parameter of type 'string'", pos, end, }); diff --git a/packages/compiler/test/checker/valueof-casting.test.ts b/packages/compiler/test/checker/valueof-casting.test.ts index c9eb7c8c04..fff3063f08 100644 --- a/packages/compiler/test/checker/valueof-casting.test.ts +++ b/packages/compiler/test/checker/valueof-casting.test.ts @@ -1,49 +1,8 @@ import { ok, strictEqual } from "assert"; import { it } from "vitest"; -import { Diagnostic, Model, Type, Value, isType, isValue } from "../../src/index.js"; -import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/expect.js"; -import { createTestHost } from "../../src/testing/test-host.js"; - -export async function compileAndDiagnoseValueOrType( - constraint: string, - code: string, - other?: string -): Promise<[Type | Value | undefined, readonly Diagnostic[]]> { - const host = await createTestHost(); - host.addTypeSpecFile( - "main.tsp", - ` - @test model Test {} - alias Instance = Test<${code}>; - - ${other ?? ""} - ` - ); - const [{ Test }, diagnostics] = await host.compileAndDiagnose("main.tsp"); - const arg = (Test as Model).templateMapper?.args[0]; - return [arg, diagnostics]; -} - -export async function compileValueOrType( - constraint: string, - code: string, - other?: string -): Promise { - const [called, diagnostics] = await compileAndDiagnoseValueOrType(constraint, code, other); - expectDiagnosticEmpty(diagnostics); - ok(called, "Decorator was not called"); - - return called; -} - -export async function diagnoseValueOrType( - constraint: string, - code: string, - other?: string -): Promise { - const [_, diagnostics] = await compileAndDiagnoseValueOrType(constraint, code, other); - return diagnostics; -} +import { isType, isValue } from "../../src/index.js"; +import { expectDiagnostics } from "../../src/testing/expect.js"; +import { compileValueOrType, diagnoseValueOrType } from "./values/utils.js"; it("extends valueof string returns a string value", async () => { const entity = await compileValueOrType("valueof string", `"hello"`); diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index 50525d35f1..d7fc483196 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -1,8 +1,8 @@ import { ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; -import { Model, isValue } from "../../../src/index.js"; -import { createTestRunner, expectDiagnostics } from "../../../src/testing/index.js"; -import { compileValue, diagnoseUsage, diagnoseValue } from "./utils.js"; +import { isValue } from "../../../src/index.js"; +import { expectDiagnostics } from "../../../src/testing/index.js"; +import { compileValue, compileValueOrType, diagnoseUsage, diagnoseValue } from "./utils.js"; it("no values", async () => { const object = await compileValue(`#[]`); @@ -86,17 +86,7 @@ describe("emit diagnostic when used in", () => { describe("(LEGACY) cast tuple to array value", () => { it("create the value", async () => { - const runner = await createTestRunner(); - const { Test } = (await runner.compile( - ` - @test model Test {} - - #suppress "deprecated" "for testing" - alias A = Test<["foo", "bar"]>; - ` - )) as { Test: Model }; - - const value = Test.templateMapper?.args[0]; + const value = await compileValueOrType(`valueof string[]`, `["foo", "bar"]`); ok(value && isValue(value)); strictEqual(value.valueKind, "ArrayValue"); expect(value.values).toHaveLength(2); @@ -129,8 +119,9 @@ describe("(LEGACY) cast tuple to array value", () => { `); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '[string]' is not assignable to type 'valueof string[]'", + code: "invalid-argument", + message: + "Argument of type '[string]' is not assignable to parameter of type 'valueof string[]'", pos, }); }); diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index 55e25cfbab..907f252a7f 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -1,8 +1,8 @@ import { ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; -import { Model, isValue } from "../../../src/index.js"; -import { createTestRunner, expectDiagnostics } from "../../../src/testing/index.js"; -import { compileValue, diagnoseUsage, diagnoseValue } from "./utils.js"; +import { isValue } from "../../../src/index.js"; +import { expectDiagnostics } from "../../../src/testing/index.js"; +import { compileValue, compileValueOrType, diagnoseUsage, diagnoseValue } from "./utils.js"; it("no properties", async () => { const object = await compileValue(`#{}`); @@ -145,17 +145,10 @@ describe("emit diagnostic when used in", () => { describe("(LEGACY) cast model to object value", () => { it("create the value", async () => { - const runner = await createTestRunner(); - const { Test } = (await runner.compile( - ` - @test model Test {} - - #suppress "deprecated" "for testing" - alias A = Test<{a: "foo", b: "bar"}>; - ` - )) as { Test: Model }; - - const value = Test.templateMapper?.args[0]; + const value = await compileValueOrType( + `valueof {a: string, b: string}`, + `{a: "foo", b: "bar"}` + ); ok(value && isValue(value)); strictEqual(value.valueKind, "ObjectValue"); expect(value.properties).toHaveLength(2); @@ -193,8 +186,9 @@ describe("(LEGACY) cast model to object value", () => { expectDiagnostics(diagnostics, [ { code: "deprecated" }, // deprecated diagnostic still emitted { - code: "unassignable", - message: "Type '{ a: string }' is not assignable to type 'valueof { a: string }'", + code: "invalid-argument", + message: + "Argument of type '{ a: string }' is not assignable to parameter of type 'valueof { a: string }'", pos, }, ]); diff --git a/packages/compiler/test/checker/values/utils.ts b/packages/compiler/test/checker/values/utils.ts index 88eba8c830..619faeb657 100644 --- a/packages/compiler/test/checker/values/utils.ts +++ b/packages/compiler/test/checker/values/utils.ts @@ -1,5 +1,5 @@ import { ok } from "assert"; -import { Diagnostic, Type, Value } from "../../../src/index.js"; +import { Diagnostic, Model, Type, Value, defineModuleFlags } from "../../../src/index.js"; import { createTestHost, createTestRunner, @@ -61,3 +61,56 @@ export async function diagnoseValue(code: string, other?: string): Promise { + const host = await createTestHost(); + host.addJsFile("collect.js", { + $collect: () => {}, + $flags: defineModuleFlags({ decoratorArgMarshalling: "lossless" }), + }); + host.addTypeSpecFile( + "main.tsp", + ` + import "./collect.js"; + extern dec collect(target, value: ${constraint}); + + #suppress "deprecated" "for testing" + @collect(${code}) + @test model Test {} + ${other ?? ""} + ` + ); + const [{ Test }, diagnostics] = (await host.compileAndDiagnose("main.tsp")) as [ + { Test: Model }, + Diagnostic[], + ]; + const dec = Test.decorators.find((x) => x.definition?.name === "@collect"); + ok(dec); + + return [dec.args[0].value, diagnostics]; +} + +export async function compileValueOrType( + constraint: string, + code: string, + other?: string +): Promise { + const [called, diagnostics] = await compileAndDiagnoseValueOrType(constraint, code, other); + expectDiagnosticEmpty(diagnostics); + ok(called, "Decorator was not called"); + + return called; +} + +export async function diagnoseValueOrType( + constraint: string, + code: string, + other?: string +): Promise { + const [_, diagnostics] = await compileAndDiagnoseValueOrType(constraint, code, other); + return diagnostics; +} diff --git a/packages/compiler/test/projection/projector-identity.test.ts b/packages/compiler/test/projection/projector-identity.test.ts index c4d37f376e..8c038bcd21 100644 --- a/packages/compiler/test/projection/projector-identity.test.ts +++ b/packages/compiler/test/projection/projector-identity.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { DecoratorContext, Namespace, Type, getTypeName, isValue } from "../../src/core/index.js"; +import { DecoratorContext, Namespace, Type, getTypeName, isType } from "../../src/core/index.js"; import { createProjector } from "../../src/core/projector.js"; import { createTestHost, createTestRunner } from "../../src/testing/test-host.js"; import { BasicTestRunner, TestHost } from "../../src/testing/types.js"; @@ -376,22 +376,22 @@ describe("compiler: projector: Identity", () => { ok(value !== original.templateMapper.map.get(key)); } for (const arg of original.templateMapper.args) { - if (!isValue(arg)) { + if (isType(arg)) { ok(arg.projector === original.projector); } } for (const value of original.templateMapper.map.values()) { - if (!isValue(value)) { + if (isType(value)) { ok(value.projector === original.projector); } } for (const arg of projected.templateMapper.args) { - if (!isValue(arg)) { + if (isType(arg)) { ok(arg.projector === projected.projector); } } for (const value of projected.templateMapper.map.values()) { - if (!isValue(value)) { + if (isType(value)) { ok(value.projector === projected.projector); } } diff --git a/packages/protobuf/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index 420c4e74ee..535d5e12c2 100644 --- a/packages/protobuf/src/transform/index.ts +++ b/packages/protobuf/src/transform/index.ts @@ -14,7 +14,7 @@ import { IntrinsicType, isDeclaredInNamespace, isTemplateInstance, - isValue, + isType, Model, ModelProperty, Namespace, @@ -538,8 +538,8 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P function mapToProto(t: Model, relativeSource: Model | Operation): ProtoMap { const [keyType, valueType] = t.templateMapper!.args; - compilerAssert(!isValue(keyType), "Cannot be a value type"); - compilerAssert(!isValue(valueType), "Cannot be a value type"); + compilerAssert(isType(keyType), "Cannot be a value type"); + compilerAssert(isType(valueType), "Cannot be a value type"); // A map's value cannot be another map. if (isMap(program, keyType)) { reportDiagnostic(program, { @@ -562,7 +562,7 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P function arrayToProto(t: Model, relativeSource: Model | Operation): ProtoType { const valueType = (t as Model).templateMapper!.args[0]; - compilerAssert(!isValue(valueType), "Cannot be a value type"); + compilerAssert(isType(valueType), "Cannot be a value type"); // Nested arrays are not supported. if (isArray(valueType)) { diff --git a/packages/versioning/src/validate.ts b/packages/versioning/src/validate.ts index fc2e700279..1a1d4a1e04 100644 --- a/packages/versioning/src/validate.ts +++ b/packages/versioning/src/validate.ts @@ -3,7 +3,7 @@ import { getService, getTypeName, isTemplateInstance, - isValue, + isType, Namespace, navigateProgram, NoTarget, @@ -474,7 +474,7 @@ function validateReference(program: Program, source: Type, target: Type) { if ("templateMapper" in target) { for (const param of target.templateMapper?.args ?? []) { - if (!isValue(param)) { + if (isType(param)) { validateTargetVersionCompatible(program, source, param); } } From 7d8441d9c9beb68a489c1a7d178fc7ad7f0a0ecc Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 10:05:18 -0700 Subject: [PATCH 137/184] Add validation of usage of template with valueof vs not --- packages/compiler/src/core/checker.ts | 49 ++++++-- packages/compiler/src/core/messages.ts | 2 + .../compiler/test/checker/relation.test.ts | 11 ++ packages/compiler/test/checker/typeof.test.ts | 118 ++++++++++++++++++ packages/compiler/test/test-utils.ts | 27 ++++ 5 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/checker/typeof.test.ts create mode 100644 packages/compiler/test/test-utils.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index f9e380111d..9acd5d8a3d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -706,6 +706,18 @@ export function createChecker(program: Program): Checker { ); return errorType; } + if (entity.kind === "TemplateParameter") { + if (entity.constraint?.valueType) { + // means this template constraint will accept values + reportCheckerDiagnostic( + createDiagnostic({ + code: "value-in-type", + messageId: "templateConstraint", + target: node, + }) + ); + } + } return entity; } @@ -1111,7 +1123,7 @@ export function createChecker(program: Program): Checker { function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: undefined - ): TemplateParameter | IndeterminateEntity; + ): TemplateParameter; function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: TypeMapper @@ -1369,7 +1381,7 @@ export function createChecker(program: Program): Checker { } const initMap = new Map( decls.map((decl) => { - const declaredType = getTypeForNode(decl)! as TemplateParameter; + const declaredType = checkTemplateParameterDeclaration(decl, undefined); positional.push(declaredType); params.set(decl.id.sv, declaredType); @@ -4096,16 +4108,38 @@ export function createChecker(program: Program): Checker { function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { const entity = getTypeOrValueForNodeInternal(node.target, mapper, undefined); if (entity === null) { - return errorType; // TODO: emit error + // Shouldn't need to emit error as we assume null value already emitted error when produced + return errorType; } if ("metaKind" in entity) { return entity.type; } + if (isType(entity)) { - return entity; // TODO: emit error - } - if (entity === null) { - return errorType; // TODO: emit error + if (entity.kind === "TemplateParameter") { + if (entity.constraint === undefined || entity.constraint.type !== undefined) { + // means this template constraint will accept values + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + messageId: "templateConstraint", + format: { name: getTypeName(entity) }, + target: node.target, + }) + ); + return errorType; + } else if (entity.constraint.valueType) { + return entity.constraint.valueType; + } + } + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity) }, + target: node.target, + }) + ); + return entity; } return entity.type; } @@ -6921,6 +6955,7 @@ export function createChecker(program: Program): Checker { "metaKind" in target && source.kind === "TemplateParameter" && source.constraint?.type && + source.constraint.valueType === undefined && target.metaKind === "MixedConstraint" && target.valueType ) { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 1cadb41126..244d411e5e 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -409,6 +409,7 @@ const diagnostics = { severity: "error", messages: { default: paramMessage`${"name"} refers to a type, but is being used as a value here.`, + templateConstraint: paramMessage`${"name"} template parameter can be a type but is being used as a value here.`, }, }, "non-callable": { @@ -447,6 +448,7 @@ const diagnostics = { severity: "error", messages: { default: "A value cannot be used as a type.", + templateConstraint: "Template parameter can be passed values but is used as a type.", }, }, "no-prop": { diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 0165e2e0a8..8bd8b55c8f 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1602,6 +1602,17 @@ describe("compiler: checker: type relations", () => { expectDiagnosticEmpty(diagnostics); }); + it("valueof X template constraint cannot be used as a type", async () => { + const diagnostics = await runner.diagnose(` + model Foo { + kind: T; + }`); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "Template parameter can be passed values but is used as a type.", + }); + }); + it("can use valueof unknown constraint not assignable to unknown", async () => { const { source, pos } = extractCursor(` model A {} diff --git a/packages/compiler/test/checker/typeof.test.ts b/packages/compiler/test/checker/typeof.test.ts new file mode 100644 index 0000000000..e4dd5dc61e --- /dev/null +++ b/packages/compiler/test/checker/typeof.test.ts @@ -0,0 +1,118 @@ +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { expectDiagnostics } from "../../src/testing/expect.js"; +import { createTestHost, createTestRunner } from "../../src/testing/test-host.js"; +import { extractSquiggles } from "../../src/testing/test-server-host.js"; +import { BasicTestRunner } from "../../src/testing/types.js"; +import { defineTest } from "../test-utils.js"; + +const { compile: compileTypeOf, diagnose: diagnoseTypeOf } = defineTest( + async (typeofCode: string, commonCode?: string) => { + const runner = await createTestRunner(); + + const [{ target }, diagnostics] = await runner.compileAndDiagnose(` + ${commonCode ?? ""} + model Test { + @test target: ${typeofCode}; + } + `); + ok(target, `Expected a property tagged with @test("target")`); + strictEqual(target.kind, "ModelProperty"); + return [target.type, diagnostics]; + } +); + +describe("get the type of a const", () => { + it("const without an explicit type return the precise type of the value", async () => { + const type = await compileTypeOf(`typeof a`, `const a = 123;`); + strictEqual(type.kind, "Number"); + strictEqual(type.value, 123); + }); + + it("const with an explicit type return const type", async () => { + const type = await compileTypeOf(`typeof a`, `const a: int32 = 123;`); + strictEqual(type.kind, "Scalar"); + strictEqual(type.name, "int32"); + }); +}); + +describe("emit errors when typeof a type", () => { + it("typeof scalar", async () => { + const diagnostics = await diagnoseTypeOf(`typeof int32`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "int32 refers to a type, but is being used as a value here.", + }); + }); + it("typeof model", async () => { + const diagnostics = await diagnoseTypeOf(`typeof A`, "model A {}"); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "A refers to a type, but is being used as a value here.", + }); + }); +}); + +describe("emit error if trying to typeof a template parameter that accept types", () => { + it.each([ + ["no constraint is equivalent `extends unknown`", ""], + ["constrained to only types", "extends string"], + ["constrained with types and value", "extends string | valueof string"], + ])("%s", async (label, constraint) => { + const runner = await createTestRunner(); + const { pos, end, source } = extractSquiggles(` + model A { + prop: typeof ~~~T~~~; + } + `); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + code: "expect-value", + pos, + end, + }); + }); +}); + +describe("typeof can be used to force sending a type to a decorator that accept both", () => { + let runner: BasicTestRunner; + let called: any; + + beforeEach(async () => { + called = undefined; + const host = await createTestHost(); + host.addJsFile("dec.js", { + $foo: (_ctx: any, _target: any, value: any) => { + called = value; + }, + }); + runner = await createTestRunner(host); + }); + + it("directly to decorator", async () => { + await runner.compile(` + import "./dec.js"; + extern dec foo(target, value: string | valueof string); + + @foo(typeof "abc") + model A {} + `); + ok(called); + strictEqual(called.kind, "String"); + strictEqual(called.value, "abc"); + }); + + it("via template", async () => { + await runner.compile(` + import "./dec.js"; + extern dec foo(target, value: string | valueof string); + + alias T = A; + @foo(T) + model A {} + `); + ok(called); + strictEqual(called.kind, "String"); + strictEqual(called.value, "abc"); + }); +}); diff --git a/packages/compiler/test/test-utils.ts b/packages/compiler/test/test-utils.ts new file mode 100644 index 0000000000..d653d8603e --- /dev/null +++ b/packages/compiler/test/test-utils.ts @@ -0,0 +1,27 @@ +import { ok } from "assert"; +import type { Diagnostic } from "../src/index.js"; +import { expectDiagnosticEmpty } from "../src/testing/expect.js"; + +export interface Test { + compile(...args: I): Promise; + compileAndDiagnose(...args: I): Promise<[R | undefined, readonly Diagnostic[]]>; + diagnose(...args: I): Promise; +} +export function defineTest( + fn: (...args: T) => Promise<[R | undefined, readonly Diagnostic[]]> +): Test { + return { + compileAndDiagnose: fn, + compile: async (...args) => { + const [called, diagnostics] = await fn(...args); + expectDiagnosticEmpty(diagnostics); + ok(called, "Decorator was not called"); + + return called; + }, + diagnose: async (...args) => { + const [_, diagnostics] = await fn(...args); + return diagnostics; + }, + }; +} From 096e2e29ebe8d8c44d25795e79cc451236424b76 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 11:38:20 -0700 Subject: [PATCH 138/184] Fix some assignment issues --- packages/compiler/src/core/checker.ts | 75 +++++++++---------- packages/compiler/test/checker/model.test.ts | 18 ++++- .../test/checker/values/const.test.ts | 7 ++ 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 9acd5d8a3d..ef98a34bb8 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -734,7 +734,7 @@ export function createChecker(program: Program): Checker { let entity: Type | Value | null; if ("metaKind" in initial) { compilerAssert(initial.metaKind === "Indeterminate", "Expected indeterminate entity"); - entity = tryUsingValueOfType(initial.type, constraint, node); + entity = getValueFromIndeterminate(initial.type, constraint, node); if (options.legacyTupleAndModelCast && entity !== null && isType(entity)) { entity = legacy_tryTypeToValueCast(entity, constraint, node); } @@ -745,20 +745,7 @@ export function createChecker(program: Program): Checker { return null; } if (isValue(entity)) { - return entity; - } - if (isType(entity)) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "expect-value", - format: { name: getTypeName(entity) }, - target: node, - }) - ); - return null; - } - if (entity === null || isValue(entity)) { - return entity; + return constraint ? inferScalarsFromConstraints(entity, constraint.type) : entity; } reportCheckerDiagnostic( createDiagnostic({ @@ -771,7 +758,7 @@ export function createChecker(program: Program): Checker { } /** In certain context for types that can also be value if the constraint allows it we try to use it as a value instead of a type. */ - function tryUsingValueOfType( + function getValueFromIndeterminate( type: Type, constraint: CheckValueConstraint | undefined, node: Node @@ -787,7 +774,7 @@ export function createChecker(program: Program): Checker { case "EnumMember": return checkEnumValue(type, constraint, node); case "UnionVariant": - return tryUsingValueOfType(type.type, constraint, node); + return getValueFromIndeterminate(type.type, constraint, node); case "Intrinsic": switch (type.name) { case "null": @@ -844,7 +831,11 @@ export function createChecker(program: Program): Checker { }; for (const prop of model.properties.values()) { - let propValue = tryUsingValueOfType(prop.type, { kind: "assignment", type: prop.type }, node); + let propValue = getValueFromIndeterminate( + prop.type, + { kind: "assignment", type: prop.type }, + node + ); if (propValue !== null && isType(propValue)) { propValue = legacy_tryTypeToValueCast( propValue, @@ -900,7 +891,7 @@ export function createChecker(program: Program): Checker { : type?.kind === "Tuple" ? type.values[index] : undefined; - let value = tryUsingValueOfType( + let value = getValueFromIndeterminate( item, itemType && { kind: "assignment", type: itemType }, node @@ -966,7 +957,7 @@ export function createChecker(program: Program): Checker { compilerAssert(entity.metaKind === "Indeterminate", "Expected indeterminate entity"); if (valueConstraint) { - return tryUsingValueOfType(entity.type, valueConstraint, node); + return getValueFromIndeterminate(entity.type, valueConstraint, node); } return entity.type; @@ -3773,7 +3764,6 @@ export function createChecker(program: Program): Checker { } function inferScalarForPrimitiveValue( - base: Scalar, type: Type | undefined, literalType: Type ): Scalar | undefined { @@ -3782,14 +3772,14 @@ export function createChecker(program: Program): Checker { } switch (type.kind) { case "Scalar": - if (areScalarsRelated(type, base)) { + if (ignoreDiagnostics(isTypeAssignableTo(literalType, type, literalType))) { return type; } return undefined; case "Union": let found = undefined; for (const variant of type.variants.values()) { - const scalar = inferScalarForPrimitiveValue(base, variant.type, literalType); + const scalar = inferScalarForPrimitiveValue(variant.type, literalType); if (scalar) { if (found) { reportCheckerDiagnostic( @@ -3834,11 +3824,7 @@ export function createChecker(program: Program): Checker { } else { value = literalType.value; } - const scalar = inferScalarForPrimitiveValue( - getStdType("string"), - constraint?.type, - literalType - ); + const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); return { valueKind: "StringValue", value, @@ -3855,11 +3841,7 @@ export function createChecker(program: Program): Checker { if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } - const scalar = inferScalarForPrimitiveValue( - getStdType("numeric"), - constraint?.type, - literalType - ); + const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); return { valueKind: "NumericValue", value: Numeric(literalType.valueAsString), @@ -3876,11 +3858,7 @@ export function createChecker(program: Program): Checker { if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { return null; } - const scalar = inferScalarForPrimitiveValue( - getStdType("boolean"), - constraint?.type, - literalType - ); + const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); return { valueKind: "BooleanValue", value: literalType.value, @@ -4809,7 +4787,7 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostics(diagnostics); return null; } else { - return defaultValue; + return { ...defaultValue, type }; } } @@ -5382,6 +5360,25 @@ export function createChecker(program: Program): Checker { return links.value; } + function inferScalarsFromConstraints(value: T, type: Type): T { + switch (value.valueKind) { + case "BooleanValue": + case "StringValue": + case "NumericValue": + if (value.scalar === undefined) { + const scalar = inferScalarForPrimitiveValue(type, value.type); + return { ...value, scalar }; + } + return value; + case "ArrayValue": + case "ObjectValue": + case "EnumValue": + case "NullValue": + case "ScalarValue": + return value; + } + } + function checkEnum(node: EnumStatementNode, mapper: TypeMapper | undefined): Type { const links = getSymbolLinks(node.symbol); if (!links.type) { diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 205fe06110..769cc6cab9 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -111,7 +111,7 @@ describe("compiler: models", () => { describe("property defaults", () => { describe("set defaultValue", () => { - const testCases: [string, string, { kind: string; value: unknown }][] = [ + const testCases: [string, string, { kind: string; value: any }][] = [ ["boolean", `false`, { kind: "BooleanValue", value: false }], ["boolean", `true`, { kind: "BooleanValue", value: true }], ["string", `"foo"`, { kind: "StringValue", value: "foo" }], @@ -128,7 +128,7 @@ describe("compiler: models", () => { ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; strictEqual(foo.defaultValue?.valueKind, expectedValue.kind); - deepStrictEqual((foo.defaultValue as any).value, expectedValue.value); + expect((foo.defaultValue as any).value).toMatchObject(expectedValue.value); }); it(`foo?: string[] = #["abc"]`, async () => { @@ -153,6 +153,20 @@ describe("compiler: models", () => { strictEqual(foo.defaultValue?.valueKind, "ObjectValue"); }); + it(`assign scalar for primitive types if not yet`, async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + const a = 123; + model A { @test foo?: int32 = a } + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + strictEqual(foo.defaultValue?.valueKind, "NumericValue"); + strictEqual(foo.defaultValue.scalar?.kind, "Scalar"); + strictEqual(foo.defaultValue.scalar?.name, "int32"); + }); + it(`foo?: Enum = Enum.up`, async () => { testHost.addTypeSpecFile( "main.tsp", diff --git a/packages/compiler/test/checker/values/const.test.ts b/packages/compiler/test/checker/values/const.test.ts index 41e7d48024..281715a630 100644 --- a/packages/compiler/test/checker/values/const.test.ts +++ b/packages/compiler/test/checker/values/const.test.ts @@ -1,5 +1,6 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; +import { NumericValue } from "../../../src/index.js"; import { expectDiagnostics } from "../../../src/testing/expect.js"; import { compileValue, diagnoseUsage } from "./utils.js"; @@ -16,6 +17,12 @@ describe("without type it use the most precise type", () => { }); }); +it("when assigning another const a primitive value that didn't figure out the scalar it resolved it then", async () => { + const value = (await compileValue("b", `const a = 123;const b: int64 = a;`)) as NumericValue; + strictEqual(value.scalar?.kind, "Scalar"); + strictEqual(value.scalar.name, "int64"); +}); + it("when assigning another const it change the type", async () => { const value = await compileValue("b", `const a: int32 = 123;const b: int64 = a;`); strictEqual(value.type.kind, "Scalar"); From ad161a139bcff51c83effe02a15fd7ba9403906c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 12:42:03 -0700 Subject: [PATCH 139/184] fix --- packages/compiler/src/core/checker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index bc4af04fa8..39557bf642 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3703,6 +3703,7 @@ export function createChecker(program: Program): Checker { properties: createRekeyableMap(), decorators: [], derivedModels: [], + sourceModels: [], }); for (const prop of properties.values()) { From ee6580735f8e0274ccadd63590ea7825ae91c1bc Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 12:58:09 -0700 Subject: [PATCH 140/184] Fix test --- packages/http/test/http-decorators.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 68a9ee9567..cc4a655b3a 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -712,10 +712,10 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { - code: "unassignable", + code: "invalid-argument", }, { - code: "unassignable", + code: "invalid-argument", }, ]); }); From 6c1b42c7027d2d54b5add45c9cea2173e7f8f621 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 13:14:33 -0700 Subject: [PATCH 141/184] fix up tests --- packages/compiler/src/core/checker.ts | 49 ++++++++----------- .../compiler/src/core/diagnostic-creator.ts | 3 +- .../src/emitter-framework/type-emitter.ts | 3 ++ 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 39557bf642..206daccdca 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -690,7 +690,7 @@ export function createChecker(program: Program): Checker { } function getTypeForNode(node: Node, mapper?: TypeMapper): Type { - const entity = getTypeOrValueOrIndeterminateForNode(node, mapper); + const entity = checkNode(node, mapper); if (entity === null) { return errorType; } @@ -727,7 +727,7 @@ export function createChecker(program: Program): Checker { constraint?: CheckValueConstraint, options: { legacyTupleAndModelCast?: boolean } = {} ): Value | null { - const initial = getTypeOrValueForNodeInternal(node, mapper, constraint); + const initial = checkNode(node, mapper, constraint); if (initial === null) { return null; } @@ -735,12 +735,12 @@ export function createChecker(program: Program): Checker { if ("metaKind" in initial) { compilerAssert(initial.metaKind === "Indeterminate", "Expected indeterminate entity"); entity = getValueFromIndeterminate(initial.type, constraint, node); - if (options.legacyTupleAndModelCast && entity !== null && isType(entity)) { - entity = legacy_tryTypeToValueCast(entity, constraint, node); - } } else { entity = initial; } + if (options.legacyTupleAndModelCast && entity !== null && isType(entity)) { + entity = legacy_tryTypeToValueCast(entity, constraint, node); + } if (entity === null) { return null; } @@ -942,7 +942,7 @@ export function createChecker(program: Program): Checker { constraint?: CheckConstraint | undefined ): Type | Value | null { const valueConstraint = extractValueOfConstraints(constraint); - const entity = getTypeOrValueForNodeInternal(node, mapper, valueConstraint); + const entity = checkNode(node, mapper, valueConstraint); if (entity === null) { return entity; } else if (isType(entity)) { @@ -963,19 +963,6 @@ export function createChecker(program: Program): Checker { return entity.type; } - /** - * Gets a type or value depending on the node and current constraint. - * For nodes that can be both type or values(e.g. string), the value will be returned if the constraint expect a value of that type even if the constrain also allows the type. - * This means that if the constraint is `string | valueof string` passing `"abc"` will send the value `"abc"` and not the type `"abc"`. - */ - function getTypeOrValueOrIndeterminateForNode( - node: Node, - mapper?: TypeMapper - ): Type | Value | IndeterminateEntity | null { - return getTypeOrValueForNodeInternal(node, mapper); - } - - // TODO: do we still need this? /** Extact the type constraint a value should match. */ function extractValueOfConstraints( constraint: CheckConstraint | undefined @@ -987,8 +974,12 @@ export function createChecker(program: Program): Checker { } } - /** Do not call to be used inside getTypeOrValueForNode */ - function getTypeOrValueForNodeInternal( + /** + * Gets a type, value or indeterminate depending on the node and current constraint. + * For nodes that can be both type or values(e.g. string literals), an indeterminate entity will be returned. + * It is the job of of the consumer to decide if it should be a type or a value depending on the context. + */ + function checkNode( node: Node, mapper?: TypeMapper, valueConstraint?: CheckValueConstraint | undefined @@ -1203,7 +1194,7 @@ export function createChecker(program: Program): Checker { constraint: Entity | undefined ): Type | Value | IndeterminateEntity { function visit(node: Node) { - const type = getTypeOrValueOrIndeterminateForNode(node); + const type = checkNode(node); let hasError = false; if (type !== null && "kind" in type && type.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { @@ -1279,7 +1270,7 @@ export function createChecker(program: Program): Checker { node: TemplateArgumentNode, mapper: TypeMapper | undefined ): Type | Value | IndeterminateEntity | null { - return getTypeOrValueOrIndeterminateForNode(node.argument, mapper); + return checkNode(node.argument, mapper); } function resolveTypeReference( @@ -1391,7 +1382,7 @@ export function createChecker(program: Program): Checker { for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { function deferredCheck(): [Node, Type | Value | IndeterminateEntity | null] { - return [arg, getTypeOrValueOrIndeterminateForNode(arg.argument, mapper)]; + return [arg, checkNode(arg.argument, mapper)]; } if (arg.name) { @@ -3226,7 +3217,7 @@ export function createChecker(program: Program): Checker { let hasType = false; let hasValue = false; const spanTypeOrValues = node.spans.map( - (span) => [span, getTypeOrValueOrIndeterminateForNode(span.expression, mapper)] as const + (span) => [span, checkNode(span.expression, mapper)] as const ); for (const [_, typeOrValue] of spanTypeOrValues) { if (typeOrValue !== null) { @@ -3421,7 +3412,7 @@ export function createChecker(program: Program): Checker { function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { - getTypeOrValueOrIndeterminateForNode(statement, undefined); + checkNode(statement, undefined); } } @@ -4126,7 +4117,7 @@ export function createChecker(program: Program): Checker { } function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { - const entity = getTypeOrValueForNodeInternal(node.target, mapper, undefined); + const entity = checkNode(node.target, mapper, undefined); if (entity === null) { // Shouldn't need to emit error as we assume null value already emitted error when produced return errorType; @@ -4840,7 +4831,7 @@ export function createChecker(program: Program): Checker { * We do do checking here we just keep existing behavior. */ function checkLegacyDefault(defaultNode: Node): Type | undefined { - const resolved = getTypeOrValueOrIndeterminateForNode(defaultNode, undefined); + const resolved = checkNode(defaultNode, undefined); if (resolved === null || isValue(resolved)) { return undefined; } @@ -4965,7 +4956,7 @@ export function createChecker(program: Program): Checker { return [ false, node.arguments.map((argNode): DecoratorArgument => { - let type = getTypeOrValueOrIndeterminateForNode(argNode, mapper) ?? errorType; + let type = checkNode(argNode, mapper) ?? errorType; if ("metaKind" in type) { type = type.type; } diff --git a/packages/compiler/src/core/diagnostic-creator.ts b/packages/compiler/src/core/diagnostic-creator.ts index 88211e9a50..b1c94eff6e 100644 --- a/packages/compiler/src/core/diagnostic-creator.ts +++ b/packages/compiler/src/core/diagnostic-creator.ts @@ -1,4 +1,3 @@ -import { mutate } from "../utils/misc.js"; import type { Program } from "./program.js"; import type { Diagnostic, @@ -58,7 +57,7 @@ export function createDiagnosticCreator> { let unspeakable = false; const parameterNames = declarationType.templateMapper.args.map((t) => { + if ("metaKind" in t) { + t = t.type; + } if (!("kind" in t)) { return undefined; } From 5575e5f88078c0782859bedc2600234062446787 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 13:18:16 -0700 Subject: [PATCH 142/184] Fix up deprecations --- packages/compiler/src/core/helpers/index.ts | 1 + .../test/helpers/string-template-utils.test.ts | 3 ++- .../json-schema/src/json-schema-emitter.ts | 18 +++++++++--------- packages/openapi3/src/schema-emitter.ts | 18 +++++++++--------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/core/helpers/index.ts b/packages/compiler/src/core/helpers/index.ts index f0df08a09a..47e2fe06ea 100644 --- a/packages/compiler/src/core/helpers/index.ts +++ b/packages/compiler/src/core/helpers/index.ts @@ -3,6 +3,7 @@ export { getLocationContext } from "./location-context.js"; export * from "./operation-utils.js"; export * from "./path-interpolation.js"; export * from "./projected-names-utils.js"; +// eslint-disable-next-line deprecation/deprecation export { stringTemplateToString } from "./string-template-utils.js"; export * from "./type-name-utils.js"; export * from "./usage-resolver.js"; diff --git a/packages/compiler/test/helpers/string-template-utils.test.ts b/packages/compiler/test/helpers/string-template-utils.test.ts index 9536cc3a89..b950b9d0ed 100644 --- a/packages/compiler/test/helpers/string-template-utils.test.ts +++ b/packages/compiler/test/helpers/string-template-utils.test.ts @@ -4,7 +4,7 @@ import { ModelProperty, stringTemplateToString } from "../../src/index.js"; import { expectDiagnosticEmpty } from "../../src/testing/expect.js"; import { createTestRunner } from "../../src/testing/test-host.js"; -describe("compiler: stringTemplateToString", () => { +describe("compiler: stringTemplateToString (deprecated)", () => { async function stringifyTemplate(template: string) { const runner = await createTestRunner(); const { value } = (await runner.compile(`model Foo { @test value: ${template}; }`)) as { @@ -12,6 +12,7 @@ describe("compiler: stringTemplateToString", () => { }; strictEqual(value.type.kind, "StringTemplate"); + // eslint-disable-next-line deprecation/deprecation return stringTemplateToString(value.type); } diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index 781b861ab1..f4e586bb74 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -31,7 +31,6 @@ import { Scalar, StringLiteral, StringTemplate, - stringTemplateToString, Tuple, Type, typespecTypeToJson, @@ -53,6 +52,7 @@ import { TypeEmitter, } from "@typespec/compiler/emitter-framework"; import { stringify } from "yaml"; +import { explainStringTemplateNotSerializable } from "../../compiler/src/core/helpers/string-template-utils.js"; import { findBaseUri, getContains, @@ -239,14 +239,14 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche } stringTemplate(string: StringTemplate): EmitterOutput { - const [value, diagnostics] = stringTemplateToString(string); - if (diagnostics.length > 0) { - this.emitter - .getProgram() - .reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" }))); - return { type: "string" }; - } - return { type: "string", const: value }; + if (string.stringValue !== undefined) { + return { type: "string", const: string.stringValue }; + } + const diagnostics = explainStringTemplateNotSerializable(string); + this.emitter + .getProgram() + .reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" }))); + return { type: "string" }; } numericLiteral(number: NumericLiteral): EmitterOutput { diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 1b2afec8cd..18ab362819 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -45,7 +45,6 @@ import { isSecret, isTemplateDeclaration, resolveEncodedName, - stringTemplateToString, } from "@typespec/compiler"; import { ArrayBuilder, @@ -70,6 +69,7 @@ import { isReadonlyProperty, shouldInline, } from "@typespec/openapi"; +import { explainStringTemplateNotSerializable } from "../../compiler/src/core/helpers/string-template-utils.js"; import { getOneOf, getRef } from "./decorators.js"; import { OpenAPI3EmitterOptions, reportDiagnostic } from "./lib.js"; import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js"; @@ -411,14 +411,14 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< } stringTemplate(string: StringTemplate): EmitterOutput { - const [value, diagnostics] = stringTemplateToString(string); - if (diagnostics.length > 0) { - this.emitter - .getProgram() - .reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" }))); - return { type: "string" }; - } - return { type: "string", enum: [value] }; + if (string.stringValue !== undefined) { + return { type: "string", const: string.stringValue }; + } + const diagnostics = explainStringTemplateNotSerializable(string); + this.emitter + .getProgram() + .reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" }))); + return { type: "string" }; } numericLiteral(number: NumericLiteral): EmitterOutput { From 86e9f6c45a0a08b0187a77e3967becdd724fbf95 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 13:31:12 -0700 Subject: [PATCH 143/184] fix --- packages/compiler/src/core/helpers/index.ts | 8 +++-- .../json-schema/src/json-schema-emitter.ts | 32 +++++++++---------- packages/openapi3/src/schema-emitter.ts | 4 +-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/core/helpers/index.ts b/packages/compiler/src/core/helpers/index.ts index 47e2fe06ea..f618743f7b 100644 --- a/packages/compiler/src/core/helpers/index.ts +++ b/packages/compiler/src/core/helpers/index.ts @@ -3,7 +3,11 @@ export { getLocationContext } from "./location-context.js"; export * from "./operation-utils.js"; export * from "./path-interpolation.js"; export * from "./projected-names-utils.js"; -// eslint-disable-next-line deprecation/deprecation -export { stringTemplateToString } from "./string-template-utils.js"; + +export { + explainStringTemplateNotSerializable, + // eslint-disable-next-line deprecation/deprecation + stringTemplateToString, +} from "./string-template-utils.js"; export * from "./type-name-utils.js"; export * from "./usage-resolver.js"; diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index f4e586bb74..8f566b53c7 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -1,11 +1,24 @@ import { BooleanLiteral, - compilerAssert, DiagnosticTarget, DuplicateTracker, - emitFile, Enum, EnumMember, + IntrinsicType, + Model, + ModelProperty, + NumericLiteral, + Program, + Scalar, + StringLiteral, + StringTemplate, + Tuple, + Type, + Union, + UnionVariant, + compilerAssert, + emitFile, + explainStringTemplateNotSerializable, getDeprecated, getDirectoryPath, getDoc, @@ -21,21 +34,9 @@ import { getPattern, getRelativePathFromDirectory, getSummary, - IntrinsicType, isArrayModelType, isNullType, - Model, - ModelProperty, - NumericLiteral, - Program, - Scalar, - StringLiteral, - StringTemplate, - Tuple, - Type, typespecTypeToJson, - Union, - UnionVariant, } from "@typespec/compiler"; import { ArrayBuilder, @@ -52,8 +53,8 @@ import { TypeEmitter, } from "@typespec/compiler/emitter-framework"; import { stringify } from "yaml"; -import { explainStringTemplateNotSerializable } from "../../compiler/src/core/helpers/string-template-utils.js"; import { + JsonSchemaDeclaration, findBaseUri, getContains, getContentEncoding, @@ -69,7 +70,6 @@ import { getPrefixItems, getUniqueItems, isJsonSchemaDeclaration, - JsonSchemaDeclaration, } from "./index.js"; import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js"; export class JsonSchemaEmitter extends TypeEmitter, JSONSchemaEmitterOptions> { diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 18ab362819..3413b284c1 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -19,6 +19,7 @@ import { UnionVariant, Value, compilerAssert, + explainStringTemplateNotSerializable, getDeprecated, getDiscriminatedUnion, getDiscriminator, @@ -69,7 +70,6 @@ import { isReadonlyProperty, shouldInline, } from "@typespec/openapi"; -import { explainStringTemplateNotSerializable } from "../../compiler/src/core/helpers/string-template-utils.js"; import { getOneOf, getRef } from "./decorators.js"; import { OpenAPI3EmitterOptions, reportDiagnostic } from "./lib.js"; import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js"; @@ -412,7 +412,7 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< stringTemplate(string: StringTemplate): EmitterOutput { if (string.stringValue !== undefined) { - return { type: "string", const: string.stringValue }; + return { type: "string", enum: [string.stringValue] }; } const diagnostics = explainStringTemplateNotSerializable(string); this.emitter From c03896969a2c47fa83c0e807d1cf6e1749657e6f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 14:52:43 -0700 Subject: [PATCH 144/184] Fix --- packages/compiler/src/core/type-utils.ts | 5 ++--- packages/spec/src/spec.emu.html | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index de3b793a37..df9d87cb42 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -4,7 +4,6 @@ import { Entity, Enum, ErrorType, - IndeterminateEntity, Interface, Model, Namespace, @@ -44,10 +43,10 @@ export function isNullType(type: Entity): type is NullType { return "kind" in type && type.kind === "Intrinsic" && type.name === "null"; } -export function isType(entity: Entity | IndeterminateEntity): entity is Type { +export function isType(entity: Entity): entity is Type { return "kind" in entity; } -export function isValue(entity: Entity | IndeterminateEntity): entity is Value { +export function isValue(entity: Entity): entity is Value { return "valueKind" in entity; } diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index cdaadacbb2..2c88c6a88c 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -340,9 +340,6 @@

Syntactic Grammar

ScalarBody : ScalarMemberList `;`? -ScalarBody : - ScalarMemberList `;`? - ScalarMemberList : ScalarMember ScalarMemberList `;` ScalarMember From 4fc490040e55569fed5425033ce1ab930f1055d5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 15:09:53 -0700 Subject: [PATCH 145/184] Fix declare const in a namespace --- packages/compiler/src/core/checker.ts | 2 +- packages/compiler/test/checker/values/const.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 206daccdca..d6333ab3fb 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2255,7 +2255,7 @@ export function createChecker(program: Program): Checker { if (node.kind === SyntaxKind.NamespaceStatement) { if (isArray(node.statements)) { - node.statements.forEach((x) => getTypeForNode(x)); + node.statements.forEach((x) => checkNode(x)); } else if (node.statements) { const subNs = checkNamespace(node.statements); type.namespaces.set(subNs.name, subNs); diff --git a/packages/compiler/test/checker/values/const.test.ts b/packages/compiler/test/checker/values/const.test.ts index 281715a630..c80749d808 100644 --- a/packages/compiler/test/checker/values/const.test.ts +++ b/packages/compiler/test/checker/values/const.test.ts @@ -29,6 +29,11 @@ it("when assigning another const it change the type", async () => { strictEqual(value.type.name, "int64"); }); +it("declare const in namespace", async () => { + const value = (await compileValue("Data.a", `namespace Data {const a = 123;}`)) as NumericValue; + strictEqual(value.value.asNumber(), 123); +}); + describe("invalid assignment", () => { async function expectInvalidAssignment(code: string) { const { diagnostics, pos, end } = await diagnoseUsage(code); From 206fb0f81188fb0218197899807254cb09abeec6 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 15:35:48 -0700 Subject: [PATCH 146/184] Push --- packages/compiler/src/core/checker.ts | 52 ++++++++++++------- .../src/core/helpers/type-name-utils.ts | 2 +- packages/compiler/src/core/parser.ts | 8 +-- packages/compiler/src/core/types.ts | 10 ++-- packages/spec/src/spec.emu.html | 14 +++-- .../decorators-signatures.ts | 12 +++-- .../tspd/src/ref-doc/emitters/markdown.ts | 12 +++-- 7 files changed, 68 insertions(+), 42 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d6333ab3fb..2a99d18d9c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -109,8 +109,8 @@ import { MemberExpressionNode, MemberNode, MemberType, - MixedConstraint, MixedFunctionParameter, + MixedParameterConstraint, Model, ModelExpressionNode, ModelIndexer, @@ -733,7 +733,6 @@ export function createChecker(program: Program): Checker { } let entity: Type | Value | null; if ("metaKind" in initial) { - compilerAssert(initial.metaKind === "Indeterminate", "Expected indeterminate entity"); entity = getValueFromIndeterminate(initial.type, constraint, node); } else { entity = initial; @@ -925,7 +924,7 @@ export function createChecker(program: Program): Checker { interface CheckConstraint { kind: "argument" | "assignment"; - constraint: MixedConstraint; + constraint: MixedParameterConstraint; } interface CheckValueConstraint { kind: "argument" | "assignment"; @@ -1816,10 +1815,10 @@ export function createChecker(program: Program): Checker { } /** Check a union expresion used in a parameter constraint, those allow the use of `valueof` as a variant. */ - function checkMixedConstraintUnion( + function checkMixedParameterConstraintUnion( node: UnionExpressionNode, mapper: TypeMapper | undefined - ): MixedConstraint { + ): MixedParameterConstraint { const values: Type[] = []; const types: Type[] = []; for (const option of node.options) { @@ -1831,7 +1830,7 @@ export function createChecker(program: Program): Checker { } } return { - metaKind: "MixedConstraint", + metaKind: "MixedParameterConstraint", node, valueType: values.length === 0 @@ -2099,7 +2098,10 @@ export function createChecker(program: Program): Checker { if (mixed) { const type = node.type ? getParamConstraintEntityForNode(node.type) - : ({ metaKind: "MixedConstraint", type: unknownType } satisfies MixedConstraint); + : ({ + metaKind: "MixedParameterConstraint", + type: unknownType, + } satisfies MixedParameterConstraint); parameterType = createType({ ...base, type, @@ -2130,14 +2132,17 @@ export function createChecker(program: Program): Checker { } } - function getParamConstraintEntityForNode(node: Expression, mapper?: TypeMapper): MixedConstraint { + function getParamConstraintEntityForNode( + node: Expression, + mapper?: TypeMapper + ): MixedParameterConstraint { switch (node.kind) { case SyntaxKind.UnionExpression: - return checkMixedConstraintUnion(node, mapper); + return checkMixedParameterConstraintUnion(node, mapper); default: const [kind, entity] = getTypeOrValueOfTypeForNode(node, mapper); return { - metaKind: "MixedConstraint", + metaKind: "MixedParameterConstraint", node: node, type: kind === "value" ? undefined : entity, valueType: kind === "value" ? entity : undefined, @@ -5011,7 +5016,7 @@ export function createChecker(program: Program): Checker { const jsMarshalling = resolveDecoratorArgMarshalling(declaration); function resolveArg( argNode: Expression, - perParamType: MixedConstraint + perParamType: MixedParameterConstraint ): DecoratorArgument | undefined { const arg = getTypeOrValueForNode(argNode, mapper, { kind: "argument", @@ -5072,7 +5077,9 @@ export function createChecker(program: Program): Checker { } /** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ - function extractRestParamConstraint(constraint: MixedConstraint): MixedConstraint | undefined { + function extractRestParamConstraint( + constraint: MixedParameterConstraint + ): MixedParameterConstraint | undefined { let valueType: Type | undefined; let type: Type | undefined; if (constraint.valueType) { @@ -5094,7 +5101,7 @@ export function createChecker(program: Program): Checker { } return { - metaKind: "MixedConstraint", + metaKind: "MixedParameterConstraint", type, valueType, }; @@ -6987,7 +6994,7 @@ export function createChecker(program: Program): Checker { source.kind === "TemplateParameter" && source.constraint?.type && source.constraint.valueType === undefined && - target.metaKind === "MixedConstraint" && + target.metaKind === "MixedParameterConstraint" && target.valueType ) { const [assignable] = isTypeAssignableToInternal( @@ -7026,12 +7033,17 @@ export function createChecker(program: Program): Checker { } if ("metaKind" in target) { - return isAssignableToMixedConstraint(source, target, diagnosticTarget, relationCache); + return isAssignableToMixedParameterConstraint( + source, + target, + diagnosticTarget, + relationCache + ); } if ( isValue(source) || - ("metaKind" in source && source.metaKind === "MixedConstraint" && source.valueType) + ("metaKind" in source && source.metaKind === "MixedParameterConstraint" && source.valueType) ) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } @@ -7119,7 +7131,7 @@ export function createChecker(program: Program): Checker { function isIndeterminateEntityAssignableTo( indeterminate: IndeterminateEntity, - target: Type | MixedConstraint, + target: Type | MixedParameterConstraint, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { @@ -7162,13 +7174,13 @@ export function createChecker(program: Program): Checker { return isValueOfTypeInternal(source, target, diagnosticTarget, relationCache); } - function isAssignableToMixedConstraint( + function isAssignableToMixedParameterConstraint( source: Entity, - target: MixedConstraint, + target: MixedParameterConstraint, diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if ("metaKind" in source && source.metaKind === "MixedConstraint") { + if ("metaKind" in source && source.metaKind === "MixedParameterConstraint") { if (source.type && target.type) { const [variantAssignable, diagnostics] = isTypeAssignableToInternal( source.type, diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 0e8fbb688c..b6fd25fb23 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -90,7 +90,7 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string return getTypeName(entity, options); } else { switch (entity.metaKind) { - case "MixedConstraint": + case "MixedParameterConstraint": return [ entity.type && getEntityName(entity.type), entity.valueType && `valueof ${getEntityName(entity.valueType)}`, diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 0defa8948e..174d39943d 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -944,7 +944,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const id = parseIdentifier(); let constraint: Expression | ValueOfExpressionNode | undefined; if (parseOptional(Token.ExtendsKeyword)) { - constraint = parseMixedConstraint(); + constraint = parseMixedParameterConstraint(); } let def: Expression | undefined; if (parseOptional(Token.Equals)) { @@ -963,7 +963,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa if (token() === Token.ValueOfKeyword) { return parseValueOfExpression(); } else if (parseOptional(Token.OpenParen)) { - const expr = parseMixedConstraint(); + const expr = parseMixedParameterConstraint(); parseExpected(Token.CloseParen); return expr; } @@ -971,7 +971,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return parseIntersectionExpressionOrHigher(); } - function parseMixedConstraint(): Expression | ValueOfExpressionNode { + function parseMixedParameterConstraint(): Expression | ValueOfExpressionNode { const pos = tokenPos(); parseOptional(Token.Bar); const node: Expression = parseValueOfExpressionOrIntersectionOrHigher(); @@ -2062,7 +2062,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const optional = parseOptional(Token.Question); let type; if (parseOptional(Token.Colon)) { - type = parseMixedConstraint(); + type = parseMixedParameterConstraint(); } return { kind: SyntaxKind.FunctionParameter, diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 9e9d7758dc..999a6abf51 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -106,7 +106,7 @@ export interface TemplatedTypeBase { * - Values * - Value Constraints */ -export type Entity = Type | Value | MixedConstraint | IndeterminateEntity; +export type Entity = Type | Value | MixedParameterConstraint | IndeterminateEntity; export type Type = | BooleanLiteral @@ -171,8 +171,8 @@ export interface Projector { projectedGlobalNamespace?: Namespace; } -export interface MixedConstraint { - readonly metaKind: "MixedConstraint"; +export interface MixedParameterConstraint { + readonly metaKind: "MixedParameterConstraint"; readonly node?: UnionExpressionNode | Expression; /** Type constraints */ @@ -679,7 +679,7 @@ export interface UnionVariant extends BaseType, DecoratedType { export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; - constraint?: MixedConstraint; + constraint?: MixedParameterConstraint; default?: Type | Value | IndeterminateEntity; } @@ -715,7 +715,7 @@ export interface FunctionParameterBase extends BaseType { export interface MixedFunctionParameter extends FunctionParameterBase { // TODO: better name? mixed: true; - type: MixedConstraint; + type: MixedParameterConstraint; } /** Represent a function parameter that represent the parameter signature(i.e the type would be the type of the value passed) */ export interface SigFunctionParameter extends FunctionParameterBase { diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 2c88c6a88c..8b2a6a875d 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -441,12 +441,18 @@

Syntactic Grammar

Identifier TemplateParameterConstraint? TemplateParameterDefault? TemplateParameterConstraint : - `extends` MixedConstraint + `extends` MixedParameterConstraint -MixedConstraint : - Expression +MixedParameterConstraint : + ValueOfExpressionOrIntersectionOrHigher + ValueOfExpressionOrIntersectionOrHigher `|` ValueOfExpressionOrIntersectionOrHigher + +ValueOfExpressionOrIntersectionOrHigher : ValueOfExpression + IntersectionExpressionOrHigher + `(` MixedParameterConstraint `)` + TemplateParameterDefault : `=` Expression @@ -594,7 +600,7 @@

Syntactic Grammar

FunctionModifiers? `fn` `(` FunctionParameterList? `)` TypeAnnotation? TypeAnnotation: - `:` MixedConstraint + `:` MixedParameterConstraint FunctionModifiers: `extern` diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index 41dcbd237c..7b54af57ce 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -1,8 +1,8 @@ import { DocTag, IntrinsicScalarName, - MixedConstraint, MixedFunctionParameter, + MixedParameterConstraint, Model, Program, Scalar, @@ -115,7 +115,9 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } /** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ - function extractRestParamConstraint(constraint: MixedConstraint): MixedConstraint | undefined { + function extractRestParamConstraint( + constraint: MixedParameterConstraint + ): MixedParameterConstraint | undefined { let valueType: Type | undefined; let type: Type | undefined; if (constraint.valueType) { @@ -137,12 +139,12 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } return { - metaKind: "MixedConstraint", + metaKind: "MixedParameterConstraint", type, valueType, }; } - function getRestTSParmeterType(constraint: MixedConstraint) { + function getRestTSParmeterType(constraint: MixedParameterConstraint) { const restItemConstraint = extractRestParamConstraint(constraint); if (restItemConstraint === undefined) { return "unknown"; @@ -150,7 +152,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat return `(${getTSParmeterType(restItemConstraint)})[]`; } - function getTSParmeterType(constraint: MixedConstraint, isTarget?: boolean): string { + function getTSParmeterType(constraint: MixedParameterConstraint, isTarget?: boolean): string { if (constraint.type && constraint.valueType) { return `${getTypeConstraintTSType(constraint.type, isTarget)} | ${getValueTSType(constraint.valueType)}`; } diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts index 7b4c33ae2d..8ec4c1a78b 100644 --- a/packages/tspd/src/ref-doc/emitters/markdown.ts +++ b/packages/tspd/src/ref-doc/emitters/markdown.ts @@ -1,4 +1,10 @@ -import { Entity, MixedConstraint, getEntityName, isType, resolvePath } from "@typespec/compiler"; +import { + Entity, + MixedParameterConstraint, + getEntityName, + isType, + resolvePath, +} from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -261,7 +267,7 @@ export class MarkdownRenderer { if (dec.parameters.length > 0) { const paramTable: string[][] = [["Name", "Type", "Description"]]; for (const param of dec.parameters) { - paramTable.push([param.name, this.mixedConstraint(param.type.type), param.doc]); + paramTable.push([param.name, this.MixedParameterConstraint(param.type.type), param.doc]); } content.push(section("Parameters", [table(paramTable), ""])); } else { @@ -273,7 +279,7 @@ export class MarkdownRenderer { return section(this.headingTitle(dec), content); } - mixedConstraint(constraint: MixedConstraint): string { + MixedParameterConstraint(constraint: MixedParameterConstraint): string { return [ ...(constraint.type ? [this.ref(constraint.type)] : []), ...(constraint.valueType ? [this.ref(constraint.valueType, "valueof ")] : []), From 88c848de940c2e092acb4eabfb9b24ad3da4096d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 15:46:40 -0700 Subject: [PATCH 147/184] Fix ide reporting diagnostics on hover --- packages/compiler/src/core/checker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 2a99d18d9c..ba5a2cba4b 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2646,7 +2646,10 @@ export function createChecker(program: Program): Checker { } compilerAssert(node.parent, "Parent expected."); - const containerType = getTypeForNode(node.parent, mapper); + const containerType = getTypeOrValueForNode(node.parent, mapper); + if (containerType === null || isValue(containerType)) { + return undefined; + } if (isAnonymous(containerType)) { return undefined; // member of anonymous type cannot be referenced. } From 80f50215ed9ca0979f6b85a48745b2dca6fc5139 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 17:38:14 -0700 Subject: [PATCH 148/184] update spec --- packages/spec/src/spec.emu.html | 36 +++++++++++++++------------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 8b2a6a875d..27132f6dcc 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -444,13 +444,8 @@

Syntactic Grammar

`extends` MixedParameterConstraint MixedParameterConstraint : - ValueOfExpressionOrIntersectionOrHigher - ValueOfExpressionOrIntersectionOrHigher `|` ValueOfExpressionOrIntersectionOrHigher - -ValueOfExpressionOrIntersectionOrHigher : - ValueOfExpression - IntersectionExpressionOrHigher - `(` MixedParameterConstraint `)` + UnionExpressionOrHigher[+InParameter] + valueof UnionExpressionOrHigher TemplateParameterDefault : @@ -479,13 +474,13 @@

Syntactic Grammar

Expression : UnionExpressionOrHigher -UnionExpressionOrHigher : - IntersectionExpressionOrHigher - `|`? UnionExpressionOrHigher `|` IntersectionExpressionOrHigher +UnionExpressionOrHigher[InParameter] : + IntersectionExpressionOrHigher[InParameter] + `|`? UnionExpressionOrHigher[InParameter] `|` IntersectionExpressionOrHigher[InParameter] -IntersectionExpressionOrHigher : -ArrayExpressionOrHigher - `&`? IntersectionExpressionOrHigher `&` ArrayExpressionOrHigher +IntersectionExpressionOrHigher[InParameter] : + ArrayExpressionOrHigher[InParameter] + `&`? IntersectionExpressionOrHigher[InParameter] `&` ArrayExpressionOrHigher[InParameter] ValueOfExpression : `valueof` Expression @@ -495,15 +490,15 @@

Syntactic Grammar

`typeof` ReferenceExpression `typeof` ParenthesizedExpression -ArrayExpressionOrHigher : - PrimaryExpression - ArrayExpressionOrHigher `[` `]` +ArrayExpressionOrHigher[InParameter] : + PrimaryExpression[InParameter] + ArrayExpressionOrHigher[InParameter] `[` `]` -PrimaryExpression : +PrimaryExpression[InParameter] : TypeOfExpression Literal CallOrReferenceExpression - ParenthesizedExpression + ParenthesizedExpression[InParameter] ObjectLiteral TupleLiteral ModelExpression @@ -537,8 +532,9 @@

Syntactic Grammar

ProjectionArguments : `(` ExpressionList? `)` -ParenthesizedExpression : - `(` Expression `)` +ParenthesizedExpression[InParameter] : + [~InParameter] `(` Expression `)` + [+InParameter] `(` MixedParameterConstraint `)` ObjectLiteral : `#{` ObjectLiteralBody? `}` From 2d729a2f8173b674ed404faf07b0d14d63c2a21c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 17:51:58 -0700 Subject: [PATCH 149/184] Fix --- packages/spec/src/spec.emu.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 27132f6dcc..c24506d0b8 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -475,12 +475,12 @@

Syntactic Grammar

UnionExpressionOrHigher UnionExpressionOrHigher[InParameter] : - IntersectionExpressionOrHigher[InParameter] - `|`? UnionExpressionOrHigher[InParameter] `|` IntersectionExpressionOrHigher[InParameter] + IntersectionExpressionOrHigher[?InParameter] + `|`? UnionExpressionOrHigher[?InParameter] `|` IntersectionExpressionOrHigher[?InParameter] IntersectionExpressionOrHigher[InParameter] : - ArrayExpressionOrHigher[InParameter] - `&`? IntersectionExpressionOrHigher[InParameter] `&` ArrayExpressionOrHigher[InParameter] + ArrayExpressionOrHigher[?InParameter] + `&`? IntersectionExpressionOrHigher[?InParameter] `&` ArrayExpressionOrHigher[?InParameter] ValueOfExpression : `valueof` Expression @@ -491,14 +491,14 @@

Syntactic Grammar

`typeof` ParenthesizedExpression ArrayExpressionOrHigher[InParameter] : - PrimaryExpression[InParameter] - ArrayExpressionOrHigher[InParameter] `[` `]` + PrimaryExpression[?InParameter] + ArrayExpressionOrHigher[?InParameter] `[` `]` PrimaryExpression[InParameter] : TypeOfExpression Literal CallOrReferenceExpression - ParenthesizedExpression[InParameter] + ParenthesizedExpression[?InParameter] ObjectLiteral TupleLiteral ModelExpression From 745d8b911ae30be8e01b93338c0615f1c1139dc4 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 25 Apr 2024 17:55:33 -0700 Subject: [PATCH 150/184] fix invalid --- packages/spec/src/spec.emu.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index c24506d0b8..62ee5ed2f0 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -485,7 +485,7 @@

Syntactic Grammar

ValueOfExpression : `valueof` Expression -TypeOfExpression +TypeOfExpression : `typeof` Literal `typeof` ReferenceExpression `typeof` ParenthesizedExpression From 533e4ba4f4dc67fb26794b2a9d192a6a34f83967 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 29 Apr 2024 15:23:30 -0700 Subject: [PATCH 151/184] Update docs/language-basics/values.md Co-authored-by: Mark Cowlishaw --- docs/language-basics/values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index ec45873f0a..1877d92fd6 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -40,7 +40,7 @@ As with object values, array values cannot contain types. If an array type defines a minimum and maximum size using the `@minValue` and `@maxValue` decorators, the compiler will validate that the array value has the appropriate number of items. For example: ```typespec -/** Can have at most 3 tags */ +/** Can have at most 2 tags */ @maxItems(2) model Tags is Array; From 56e88cbd42761e1a0698dafb4db1d734502101a7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 May 2024 09:59:52 -0700 Subject: [PATCH 152/184] Add const hovering --- packages/compiler/src/core/checker.ts | 4 ++++ packages/compiler/src/server/type-signature.ts | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 49ecc54cb7..60c3190746 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -300,6 +300,9 @@ export interface Checker { /** @internal */ getValueForNode(node: Node): Value | null; + /** @internal */ + getTypeOrValueForNode(node: Node): Type | Value | null; + readonly errorType: ErrorType; readonly voidType: VoidType; readonly neverType: NeverType; @@ -455,6 +458,7 @@ export function createChecker(program: Program): Checker { getStdType, resolveTypeReference, getValueForNode, + getTypeOrValueForNode, }; const projectionMembers = createProjectionMembers(checker); diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index 49ce850849..9c650f6725 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -15,6 +15,7 @@ import { SyntaxKind, Type, UnionVariant, + Value, } from "../core/types.js"; import { printId } from "../formatter/print/printer.js"; @@ -25,8 +26,19 @@ export function getSymbolSignature(program: Program, sym: Sym): string { case SyntaxKind.AliasStatement: return fence(`alias ${getAliasSignature(decl)}`); } - const type = sym.type ?? program.checker.getTypeForNode(decl); - return getTypeSignature(type); + const entity = sym.type ?? program.checker.getTypeOrValueForNode(decl); + return getEntitySignature(sym, entity); +} + +function getEntitySignature(sym: Sym, entity: Type | Value | null): string { + if (entity === null) { + return "(error)"; + } + if ("valueKind" in entity) { + return fence(`const ${sym.name}: ${getTypeName(entity.type)}`); + } + + return getTypeSignature(entity); } function getTypeSignature(type: Type): string { From 68bdfefd443af9dcef1993ff9fabb0258ae23732 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 May 2024 10:02:01 -0700 Subject: [PATCH 153/184] add hover tests --- .../compiler/test/server/get-hover.test.ts | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/compiler/test/server/get-hover.test.ts b/packages/compiler/test/server/get-hover.test.ts index d0d5dbc53a..0594668a4c 100644 --- a/packages/compiler/test/server/get-hover.test.ts +++ b/packages/compiler/test/server/get-hover.test.ts @@ -4,7 +4,7 @@ import { Hover, MarkupKind } from "vscode-languageserver/node.js"; import { createTestServerHost, extractCursor } from "../../src/testing/test-server-host.js"; describe("compiler: server: on hover", () => { - describe("get hover for scalar", () => { + describe("scalar", () => { it("scalar declaration", async () => { const hover = await getHoverAtCursor( ` @@ -35,7 +35,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for enum", () => { + describe("enum", () => { it("normal enum", async () => { const hover = await getHoverAtCursor( ` @@ -76,7 +76,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for alias", () => { + describe("alias", () => { it("test alias declaration", async () => { const hover = await getHoverAtCursor( ` @@ -109,7 +109,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for decorator", () => { + describe("decorator", () => { it("test decorator", async () => { const hover = await getHoverAtCursor( ` @@ -137,7 +137,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for namespace", () => { + describe("namespace", () => { it("normal namespace", async () => { const hover = await getHoverAtCursor( ` @@ -173,7 +173,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for model", () => { + describe("model", () => { it("model declaration", async () => { const hover = await getHoverAtCursor( ` @@ -273,7 +273,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for interface", () => { + describe("interface", () => { it("interface declaration", async () => { const hover = await getHoverAtCursor( ` @@ -328,7 +328,7 @@ describe("compiler: server: on hover", () => { }); }); - describe("get hover for operation", () => { + describe("operation", () => { it("operation declaration", async () => { const hover = await getHoverAtCursor( ` @@ -448,6 +448,37 @@ describe("compiler: server: on hover", () => { }); }); + describe("const", () => { + it("declaration", async () => { + const hover = await getHoverAtCursor( + ` + const a┆bc = #{ a: 123 }; + ` + ); + deepStrictEqual(hover, { + contents: { + kind: MarkupKind.Markdown, + value: "```typespec\n" + "const abc: { a: 123 }\n" + "```", + }, + }); + }); + + it("reference", async () => { + const hover = await getHoverAtCursor( + ` + const abc = #{ a: 123 }; + const def = a┆bc; + ` + ); + deepStrictEqual(hover, { + contents: { + kind: MarkupKind.Markdown, + value: "```typespec\n" + "const abc: { a: 123 }\n" + "```", + }, + }); + }); + }); + async function getHoverAtCursor(sourceWithCursor: string): Promise { const { source, pos } = extractCursor(sourceWithCursor); const testHost = await createTestServerHost(); From 64b31c250d8059993e78d708f41673a772783322 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 May 2024 13:19:05 -0700 Subject: [PATCH 154/184] String literal assignable to string template --- packages/compiler/src/core/checker.ts | 6 ++++++ .../compiler/src/core/helpers/type-name-utils.ts | 10 +++++++++- packages/compiler/test/checker/relation.test.ts | 13 +++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 60c3190746..a2bc346c4e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -7310,6 +7310,12 @@ export function createChecker(program: Program): Checker { (source.kind === "StringTemplate" && source.stringValue === target.value) ); } + if (target.kind === "StringTemplate" && target.stringValue) { + return ( + (source.kind === "String" && source.value === target.stringValue) || + (source.kind === "StringTemplate" && source.stringValue === target.stringValue) + ); + } if (target.kind === "Number") { return source.kind === "Number" && target.value === source.value; } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index b6fd25fb23..46d95bd856 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -11,6 +11,7 @@ import type { Namespace, Operation, Scalar, + StringTemplate, Type, Union, Value, @@ -48,7 +49,7 @@ export function getTypeName(type: Type, options?: TypeNameOptions): string { case "Tuple": return "[" + type.values.map((x) => getTypeName(x, options)).join(", ") + "]"; case "StringTemplate": - return "string"; + return getStringTemplateName(type); case "String": return `"${type.value}"`; case "Number": @@ -242,3 +243,10 @@ function getOperationName(op: Operation, options: TypeNameOptions | undefined) { function getIdentifierName(name: string, options: TypeNameOptions | undefined) { return options?.printable ? printId(name) : name; } + +function getStringTemplateName(type: StringTemplate): string { + if (type.stringValue) { + return `"${type.stringValue}"`; + } + return "string"; +} diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 8bd8b55c8f..ad5dd68d90 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -395,6 +395,19 @@ describe("compiler: checker: type relations", () => { }); }); + describe("string template target (serializable as string)", () => { + it("can assign string literal", async () => { + await expectTypeAssignable({ source: `"foo 123 bar"`, target: `"foo \${123} bar"` }); + }); + + it("can assign string template with primitives interpolated", async () => { + await expectTypeAssignable({ + source: `"foo \${123} \${"bar"}"`, + target: `"foo \${123} bar"`, + }); + }); + }); + describe("int8 target", () => { it("can assign int8", async () => { await expectTypeAssignable({ source: "int8", target: "int8" }); From eed204bd0539bdeb0269063b499b2b6590a22776 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 May 2024 13:30:20 -0700 Subject: [PATCH 155/184] add validation for interpolating invalid template parmater --- packages/compiler/src/core/checker.ts | 20 +++++++++- .../test/checker/string-template.test.ts | 38 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index a2bc346c4e..cfa3ebd16c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3235,6 +3235,17 @@ export function createChecker(program: Program): Checker { if (typeOrValue !== null) { if (isValue(typeOrValue)) { hasValue = true; + } else if ("kind" in typeOrValue && typeOrValue.kind === "TemplateParameter") { + if (typeOrValue.constraint) { + if (typeOrValue.constraint.valueType) { + hasValue = true; + } + if (typeOrValue.constraint.type) { + hasType = true; + } + } else { + hasType = true; + } } else { hasType = true; } @@ -3254,8 +3265,13 @@ export function createChecker(program: Program): Checker { if (hasValue) { let str = node.head.value; for (const [span, typeOrValue] of spanTypeOrValues) { - compilerAssert(typeOrValue !== null && isValue(typeOrValue), "Expected value."); - str += stringifyValueForTemplate(typeOrValue); + if ( + typeOrValue !== null && + (!("kind" in typeOrValue) || typeOrValue.kind !== "TemplateParameter") + ) { + compilerAssert(typeOrValue !== null && isValue(typeOrValue), "Expected value."); + str += stringifyValueForTemplate(typeOrValue); + } str += span.literal.value; } return checkStringValue(createLiteralType(str), undefined, node); diff --git a/packages/compiler/test/checker/string-template.test.ts b/packages/compiler/test/checker/string-template.test.ts index 2ef1f1f018..800052edd3 100644 --- a/packages/compiler/test/checker/string-template.test.ts +++ b/packages/compiler/test/checker/string-template.test.ts @@ -1,7 +1,12 @@ import { strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { Model, StringTemplate } from "../../src/index.js"; -import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.js"; +import { + BasicTestRunner, + createTestRunner, + expectDiagnostics, + extractSquiggles, +} from "../../src/testing/index.js"; let runner: BasicTestRunner; @@ -101,3 +106,34 @@ describe("emit error if interpolating value in a context where template is used }); }); }); + +it("emit error if interpolating template parameter that can be a type or value", async () => { + const { source, pos, end } = extractSquiggles(` + alias Template = { + a: ~~~"\${T}"~~~; + }; + `); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + code: "mixed-string-template", + message: + "String template is interpolating values and types. It must be either all values to produce a string value or or all types for string template type.", + pos, + end, + }); +}); + +it("emit error if interpolating template parameter that is a value but using template parmater as a type", async () => { + const { source, pos, end } = extractSquiggles(` + alias Template = { + a: ~~~"\${T}"~~~; + }; + `); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + pos, + end, + }); +}); From da4ab8d583c47c761a988e1ab1679f35f5e0b5e5 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Fri, 3 May 2024 18:29:28 -0700 Subject: [PATCH 156/184] More value docs (#9) * add more docs * finish docs * Add some notes about typeof * tweak --- docs/extending-typespec/basics.md | 20 ++++-- docs/extending-typespec/create-decorators.md | 70 +++++++++--------- docs/language-basics/decorators.md | 2 +- docs/language-basics/scalars.md | 16 ++++- docs/language-basics/templates.md | 42 +++++++++++ docs/language-basics/values.md | 76 ++++++++++++++++++-- 6 files changed, 181 insertions(+), 45 deletions(-) diff --git a/docs/extending-typespec/basics.md b/docs/extending-typespec/basics.md index 6928722ab7..af1c749488 100644 --- a/docs/extending-typespec/basics.md +++ b/docs/extending-typespec/basics.md @@ -106,7 +106,7 @@ Open `./src/lib.ts` and create your library definition that registers your libra If `$lib` is not accessible from your library package (for example, `import {$lib} from "my-library";`), some features such as linting and emitter option validation will not be available. ::: -Here's an example: +For example: ```typescript import { createTypeSpecLibrary } from "@typespec/compiler"; @@ -122,7 +122,19 @@ export const { reportDiagnostic, createDiagnostic } = $lib; Diagnostics are used for linters and decorators, which are covered in subsequent topics. -### f. Create `index.ts` +### f. Set package flags + +You can optionally set any package flags by exporting a `$flags` const that is initialized with the `definePackageFlags`. Like `$lib`, this value must be exported from your package. + +It is strongly recommended to set `valueMarshalling` to `"new"` as this will be the default behavior in future TypeSpec versions. + +```typescript +export const $flags = definePackageFlags({ + valueMarshalling: "new", +}); +``` + +### g. Create `index.ts` Open `./src/index.ts` and import your library definition: @@ -131,7 +143,7 @@ Open `./src/index.ts` and import your library definition: export { $lib } from "./lib.js"; ``` -### g. Build TypeScript +### h. Build TypeScript TypeSpec can only import JavaScript files, so any changes made to TypeScript sources need to be compiled before they are visible to TypeSpec. To do this, run `npx tsc -p .` in your library's root directory. If you want to re-run the TypeScript compiler whenever files are changed, you can run `npx tsc -p . --watch`. @@ -148,7 +160,7 @@ Alternatively, you can add these as scripts in your `package.json` to make them You can then run `npm run build` or `npm run watch` to build or watch your library. -### h. Add your main TypeSpec file +### i. Add your main TypeSpec file Open `./lib/main.tsp` and import your JS entrypoint. This ensures that when TypeSpec imports your library, the code to define the library is run. When we add decorators in later topics, this import will ensure those get exposed as well. diff --git a/docs/extending-typespec/create-decorators.md b/docs/extending-typespec/create-decorators.md index 717c917f1e..c4d5275e01 100644 --- a/docs/extending-typespec/create-decorators.md +++ b/docs/extending-typespec/create-decorators.md @@ -35,7 +35,7 @@ using TypeSpec.Reflection; extern dec track(target: Model | Enum); ``` -### Optional parameters +## Optional parameters You can mark a decorator parameter as optional using `?`. @@ -43,7 +43,7 @@ You can mark a decorator parameter as optional using `?`. extern dec track(target: Model | Enum, name?: valueof string); ``` -### Rest parameters +## Rest parameters You can prefix the last parameter of a decorator with `...` to collect all the remaining arguments. The type of this parameter must be an `array expression`. @@ -51,28 +51,25 @@ You can prefix the last parameter of a decorator with `...` to collect all the r extern dec track(target: Model | Enum, ...names: valueof string[]); ``` -## Requesting a value type +## Value parameters -It's common for decorator parameters to expect a value (e.g., a string or a number). However, using `: string` as the type would also allow a user of the decorator to pass `string` itself or a custom scalar extending string, as well as a union of strings. Instead, the decorator can use `valueof ` to specify that it expects a value of that kind. - -| Example | Description | -| ----------------- | ----------------- | -| `valueof string` | Expects a string | -| `valueof float64` | Expects a float | -| `valueof int32` | Expects a number | -| `valueof boolean` | Expects a boolean | +A decorator parameter can receive [values](../language-basics/values.md) by using the `valueof` operator. For example the parameter `valueof string` expects a string value. Values are provided to the decorator implementation according the [decorator parameter marshalling](#decorator-parameter-marshalling) rules. ```tsp extern dec tag(target: unknown, value: valueof string); -// bad +// error: string is not a value @tag(string) -// good -@tag("This is the tag name") +// ok, a string literal can be a value +@tag("widgets") + +// ok, passing a value from a const +const tagName: string = "widgets"; +@tag(tagName) ``` -## Implement the decorator in JavaScript +## JavaScript decorator implementation Decorators can be implemented in JavaScript by prefixing the function name with `$`. A decorator function must have the following parameters: @@ -89,7 +86,7 @@ export function $logType(context: DecoratorContext, target: Type, name: valueof } ``` -Or in pure JavaScript: +Or in JavaScript: ```ts // model.js @@ -113,26 +110,35 @@ model Dog { ### Decorator parameter marshalling -For certain TypeSpec types (Literal types), the decorator does not receive the actual type but a marshalled value if the decorator parameter type is a `valueof`. This simplifies the most common cases. +When decorators are passed types, the type is passed as-is. When a decorator is passed a TypeSpec value, the decorator receives a JavaScript value with a type that is appropriate for representing that value. -| TypeSpec Type | Marshalled value in JS | -| ----------------- | ---------------------- | -| `valueof string` | `string` | -| `valueof numeric` | `number` | -| `valueof boolean` | `boolean` | +:::note +This behavior depends on the value of the `valueMarshalling` [package flag](../extending-typespec/basics.md#f-set-package-flags). This section describes the behavior when `valueMarshalling` is set to `"new"`. In a future release this will become the default value marshalling so it is strongly recommended to set this flag. But for now, the default value marshalling is `"legacy"` which is described in the next section. In a future release the `valueMarshalling` flag will need to be set to `"legacy"` to keep the previous marshalling behavior, but the flag will eventually be removed entirely. +::: -For all other types, they are not transformed. +| TypeSpec value type | Marshalled type in JS | +| ------------------- | --------------------------------- | +| `string` | `string` | +| `boolean` | `boolean` | +| `numeric` | `Numeric` or `number` (see below) | +| `null` | `null` | +| enum member | `EnumMemberValue` | -Example: +When marshalling numeric values, either the `Numeric` wrapper type is used, or a `number` is passed directly, depending on whether the value can be represented as a JavaScript number without precision loss. In particular, the types `numeric`, `integer`, `decimal`, `float`, `int64`, `uint64`, and `decimal128` are marshalled as a `Numeric` type. All other numeric types are marshalled as `number`. -```ts -export function $tag( - context: DecoratorContext, - target: Type, - stringArg: string, // Here instead of receiving a `StringLiteral`, the string value is being sent. - modelArg: Model // Model has no special handling so we receive the Model type -) {} -``` +When marshalling custom scalar subtypes, the marshalling behavior of the known supertype is used. For example, a `scalar customScalar extends numeric` will marshal as a `Numeric`, regardless of any value constraints that might be present. + +#### Legacy value marshalling + +With legacy value marshalling, TypeSpec strings, numbers, and booleans values are always marshalled as JS values. All other values are marshalled as their corresponding type. For example, `null` is marshalled as `NullType`. + +| TypeSpec Value Type | Marshalled value in JS | +| ------------------- | ---------------------- | +| `string` | `string` | +| `numeric` | `number` | +| `boolean` | `boolean` | + +Note that with legacy marshalling, because JavaScript numbers have limited range and precision, it is possible to define values in TypeSpec that cannot be accurately represented in JavaScript. #### String templates and marshalling diff --git a/docs/language-basics/decorators.md b/docs/language-basics/decorators.md index dc8a64dda4..aa57714ad5 100644 --- a/docs/language-basics/decorators.md +++ b/docs/language-basics/decorators.md @@ -61,4 +61,4 @@ model Dog { ## Creating decorators -_For more information on creating decorators, see the [Creating Decorators Documentation](../extending-typespec/create-decorators.md)._ +For more information on creating decorators, see [Creating Decorators](../extending-typespec/create-decorators.md). diff --git a/docs/language-basics/scalars.md b/docs/language-basics/scalars.md index 78a5009c8e..13b295ece8 100644 --- a/docs/language-basics/scalars.md +++ b/docs/language-basics/scalars.md @@ -22,9 +22,23 @@ scalar Password extends string; ## Scalars with template parameters -Scalars can also support template parameters. However, it's important to note that these templates are primarily used for decorators. +Scalars can also support template parameters. These template parameters are primarily used for decorators. ```typespec @doc(Type) scalar Unreal; ``` + +## Scalar initializers + +Scalars can be declared with an initializer for creating specific scalar values based on other values. For example: + +```typespec +scalar ipv4 extends string { + init fromInt(value: uint32); +} + +const homeIp = ipv4.fromInt(2130706433); +``` + +Initializers do not have any runtime code associated with them. Instead, they merely record the scalar initializer invoked along with the arguments passed so that emitters can construct the proper value when needed. diff --git a/docs/language-basics/templates.md b/docs/language-basics/templates.md index 5868f8b554..6bd402143a 100644 --- a/docs/language-basics/templates.md +++ b/docs/language-basics/templates.md @@ -108,3 +108,45 @@ alias Example3 = Test< Since template arguments can be specified by name, the names of template parameters are part of the template's public API. **Renaming a template parameter may break existing specifications that use the template.** **Note**: Template arguments are evaluated in the order the parameters are defined in the template _definition_, not the order in which they are written in the template _instance_. While this is usually inconsequential, it may be important in some cases where evaluating a template argument may trigger decorators with side effects. + +## Templates with values + +Templates can be declared to accept values using a `valueof` constraint. This is useful for providing default values and parameters for decorators that take values. + +```typespec +alias TakesValue = { + @doc(StringValue) + property: StringType; +}; + +alias M1 = TakesValue<"a", "b">; +``` + +When a passing a literal or an enum or union member reference directly as a template parameter that accepts either a type or a value, we pass the value. In particular, `StringTypeOrValue` is a value with the string literal type `"a"`. + +```typespec +alias TakesTypeOrValue = { + @customDecorator(StringOrValue) + property: string; +}; + +alias M1 = TakesValue<"a">; +``` + +The [`typeof` operator](./values.md#the-typeof-operator) can be used to get the declared type of a value if needed. + +### Template parameter value types + +When a template is instantiated with a value, the type of the value and the result of the `typeof` operator is determined based on the argument rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of `property` in `M1` is `"a" | "b"`. + +```typespec +alias TakesValue< + StringValue extends valueof string +> = { + @doc(StringValue) + property: typeof StringValue; +}; + +const str: "a" | "b" = "a"; +alias M1 = TakesValue; +``` diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 1877d92fd6..06a2ae4240 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -5,11 +5,15 @@ title: Values # Values -TypeSpec can define values in addition to types. Values are useful in an API description to define default values for types or provide example values. They are also useful when passing data to decorators, and for template parameters that are ultimately passed to decorators. +TypeSpec can define values in addition to types. Values are useful in an API description to define default values for types or provide example values. They are also useful when passing data to decorators, and for template parameters that are ultimately passed to decorators or used as default values. -There are three kinds of values: objects, arrays, and scalars. These values can be created with object literals, array literals, and scalar literals and initializers. Additionally, values can result from referencing enum members and union variants. +Values cannot be used as types, and types cannot be used as values, they are completely separate. However, string, number, boolean, and null literals can be either a type or a value depending on context (see also [scalar literals](#scalar-literals)). Additionally, union and enum member references may produce a type or a value depending on context (see also [enum member & union variant references](#enum-member--union-variant-references)). -## Object values +## Value kinds + +There are four kinds of values: objects, arrays, scalars. and null. These values can be created with object literals, array literals, scalar literals and initializers, and the null literal respectively. Additionally, values can result from referencing enum members and union variants. + +### Object values Object values use the syntax `#{}` and can define any number of properties. For example: @@ -27,7 +31,7 @@ const example = #{ } ``` -## Array values +### Array values Array values use the syntax `#[]` and can define any number of items. For example: @@ -48,11 +52,11 @@ const exampleTags1: Tags = #["TypeSpec", "JSON"]; // ok const exampleTags2: Tags = #["TypeSpec", "JSON", "OpenAPI"]; // error ``` -## Scalar values +### Scalar values There are two ways to create scalar values: with a literal syntax like `"string value"`, and with a scalar initializer like `utcDateTime.fromISO("2020-12-01T12:00:00Z")`. -### Scalar literals +#### Scalar literals The literal syntax for strings, numerics, booleans and null can evaluate to either a type or a value depending on the surrounding context of the literal. When the literal is in _type context_ (a model property type, operation return type, alias definition, etc.) the literal becomes a literal type. When the literal is in _value context_ (a default value, property of an object value, const definition, etc.), the literal becomes a value. When the literal is in an _ambiguous context_ (e.g. an argument to a template or decorator that can accept either a type or a value) the literal becomes a value. The `typeof` operator can be used to convert the literal to a type in such ambiguous contexts. @@ -67,7 +71,7 @@ extern dec setNumberTypeOrValue(target: unknown, color: numeric | (valueof numer model A {} ``` -### Scalar initializers +#### Scalar initializers Scalar initializers create scalar values by calling an initializer with one or more values. Scalar initializers for types extended from `numeric`, `string`, and `boolean` are called by adding parenthesis after the scalar reference: @@ -86,6 +90,64 @@ scalar ipv4 extends string { const ip = ipv4.fromInt(2341230); ``` +#### Null values + +Null values are created with the `null` literal. + +```typespec +const value: string | null = null; +``` + +The `null` value, like the `null` type, doesn't have any special behavior in the TypeSpec language. It is just the value `null` like that in JSON. + +## Const declarations + +Const declarations allow storing values in a variable for later reference. Const declarations have an optional type annotation. When the type annotation is absent, the type is inferred from the value by constructing an exact type from the initializer. + +```typespec +const stringValue: string = "hello"; +// ^-- type: string + +const oneValue = 1; +// ^-- type: 1 + +const objectValue = #{ x: 0, y: 0 }; +// ^-- type: { x: 0, y: 0 } +``` + +## The `typeof` operator + +The `typeof` operator returns the declared or inferred type of a value reference. Note that the actual value being stored by the referenced variable may be more specific than the declared type of the value. For example, if a const is declared with a union type, the value will only ever store one specific variant at a time, but typeof will give you the declared union type. + +```typespec +const stringValue: string = "hello"; +// typeof stringValue returns `string`. + +const oneValue = 1; +// typeof stringValue returns `1` + +const stringOrOneValue: string | 1 = 1; +// typeof stringOrOneValue returns `string | 1` +``` + +## Validation + +TypeSpec will validate values against built-in validation decorators like `@minLength` and `@maxValue`. + +```typespec +@maxLength(3) +scalar shortString extends string; + +const s1: shortString = "abc"; // ok +const s2: shortString = "abcd"; // error: + +model Entity { + a: shortString; +} + +const e1: Entity = #{ a: "abcd" } // error +``` + ## Enum member & union variant references References to enum members and union variants can be either types or values and follow the same rules as scalar literals. When an enum member reference is in _type context_, the reference becomes an enum member type. When in _value context_ or _ambiguous context_ the reference becomes the enum member's value. From e0d7bf5238e015e32427a3df48281153117add83 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 10:34:22 -0700 Subject: [PATCH 157/184] More --- ...ature-object-literals-2024-2-15-18-36-3-1.md | 17 +++++++++++++++++ ...ature-object-literals-2024-2-15-18-36-3-2.md | 17 +++++++++++++++++ ...feature-object-literals-2024-2-15-18-36-3.md | 17 +++++++++++++++-- ...feature-object-literals-2024-3-16-10-38-3.md | 4 ++-- docs/language-basics/templates.md | 4 +--- docs/language-basics/values.md | 2 +- packages/compiler/src/core/checker.ts | 14 ++++++-------- packages/compiler/src/core/index.ts | 2 +- packages/compiler/src/core/library.ts | 2 +- packages/compiler/src/core/types.ts | 2 +- .../compiler/test/checker/decorators.test.ts | 2 +- packages/compiler/test/checker/relation.test.ts | 6 +++--- packages/compiler/test/checker/values/utils.ts | 4 ++-- packages/json-schema/src/lib.ts | 6 +++--- .../decorators-signatures.test.ts | 6 +++--- 15 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 .chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md create mode 100644 .chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md new file mode 100644 index 0000000000..b8e9fe10e1 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md @@ -0,0 +1,17 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: deprecation +packages: + - "@typespec/compiler" +--- + +Using a tuple expression instead of a tuple literal when expecting a value is deprecated. A codefix will be provided to automatically convert tuple expressions into a literal. + +```tsp +model Test { + // Deprecated + values: string[] = ["a", "b", "c"]; + + // Correct + values: string[] = #["a", "b", "c"]; +``` diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md new file mode 100644 index 0000000000..82c656b20b --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md @@ -0,0 +1,17 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: deprecation +packages: + - "@typespec/compiler" +--- + +Using a model expression instead of an object literal when expecting a value is deprecated. A codefix will be provided to automatically convert the model expression into a literal. + +```tsp +model Test { + // Deprecated + user: {name: string} = {name: "System"}; + + // Correct + user: {name: string} = #{name: "System"}; +``` diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md index 4d06c1a9f6..a61db8af69 100644 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md @@ -1,12 +1,13 @@ --- # Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix +changeKind: feature packages: - "@typespec/compiler" --- -New Language Feature: Object and Tuple Literals. +Values In TypeSpec [See docs](https://tspwebsitepr.z22.web.core.windows.net/prs/3022/docs/next/language-basics/values) +Object and array literals ```tsp @dummy(#{ name: "John", @@ -15,3 +16,15 @@ New Language Feature: Object and Tuple Literals. aliases: #["Bob", "Frank"] }) ``` + +Scalar constructors + +```tsp +scalar utcDateTime { + init fromISO(value: string); +} + +model DateRange { + minDate: utcDateTime = utcDateTime.fromISO("2024-02-15T18:36:03Z"); +} +``` diff --git a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md index e56082e399..e20e1ae8b8 100644 --- a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md +++ b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md @@ -18,7 +18,7 @@ Decorator API: Legacy marshalling logic To opt-in you can add the following to your library js/ts files. ```ts - export const $flags = defineModuleFlags({ - decoratorArgMarshalling: "lossless", + export const $flags = definePackageFlags({ + decoratorArgMarshalling: "new", }); ``` diff --git a/docs/language-basics/templates.md b/docs/language-basics/templates.md index 6bd402143a..857b93de8d 100644 --- a/docs/language-basics/templates.md +++ b/docs/language-basics/templates.md @@ -140,9 +140,7 @@ The [`typeof` operator](./values.md#the-typeof-operator) can be used to get the When a template is instantiated with a value, the type of the value and the result of the `typeof` operator is determined based on the argument rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of `property` in `M1` is `"a" | "b"`. ```typespec -alias TakesValue< - StringValue extends valueof string -> = { +alias TakesValue = { @doc(StringValue) property: typeof StringValue; }; diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 06a2ae4240..17b539d663 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -145,7 +145,7 @@ model Entity { a: shortString; } -const e1: Entity = #{ a: "abcd" } // error +const e1: Entity = #{ a: "abcd" }; // error ``` ## Enum member & union variant references diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index cfa3ebd16c..c4de0c3e87 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1987,7 +1987,7 @@ export function createChecker(program: Program): Checker { [ `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting null as a type.`, `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "lossless"}}` to your library.', + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library.', ].join("\n"), param.node ); @@ -2002,7 +2002,7 @@ export function createChecker(program: Program): Checker { [ `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting a numeric type that is not representable as a JS Number.`, `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "lossless"}}` to your library.', + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library.', ].join("\n"), param.node ); @@ -4936,13 +4936,11 @@ export function createChecker(program: Program): Checker { }; } - function resolveDecoratorArgMarshalling( - declaredType: Decorator | undefined - ): "lossless" | "legacy" { + function resolveDecoratorArgMarshalling(declaredType: Decorator | undefined): "new" | "legacy" { if (declaredType) { const location = getLocationContext(program, declaredType); if (location.type === "compiler") { - return "lossless"; + return "new"; } else if ( (location.type === "library" || location.type === "project") && location.flags?.decoratorArgMarshalling @@ -4952,7 +4950,7 @@ export function createChecker(program: Program): Checker { return "legacy"; } } - return "lossless"; + return "new"; } /** Check the decorator target is valid */ @@ -5138,7 +5136,7 @@ export function createChecker(program: Program): Checker { function resolveDecoratorArgJsValue( value: Type | Value, valueConstraint: CheckValueConstraint | undefined, - jsMarshalling: "legacy" | "lossless" + jsMarshalling: "legacy" | "new" ) { if (valueConstraint !== undefined) { if (isValue(value)) { diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index b210b09037..c410fd9c64 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -31,7 +31,7 @@ export { createLinterRule as createRule, createTypeSpecLibrary, defineLinter, - defineModuleFlags, + definePackageFlags, paramMessage, // eslint-disable-next-line deprecation/deprecation setCadlNamespace, diff --git a/packages/compiler/src/core/library.ts b/packages/compiler/src/core/library.ts index 8c8ae0b1a1..255332beb4 100644 --- a/packages/compiler/src/core/library.ts +++ b/packages/compiler/src/core/library.ts @@ -103,7 +103,7 @@ export function createTypeSpecLibrary< } } -export function defineModuleFlags(flags: ModuleFlags): ModuleFlags { +export function definePackageFlags(flags: ModuleFlags): ModuleFlags { return flags; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 999a6abf51..5cfdff7df9 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -2398,7 +2398,7 @@ export interface ModuleFlags { * - null value -> `NullType` * @default legacy */ - readonly decoratorArgMarshalling?: "legacy" | "lossless"; + readonly decoratorArgMarshalling?: "legacy" | "new"; } export interface LinterDefinition { diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 8e7b7c6f78..e8cd4def16 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -328,7 +328,7 @@ describe("compiler: checker: decorators", () => { value: string, suppress?: boolean ): Promise { - mutate($flags).decoratorArgMarshalling = "lossless"; + mutate($flags).decoratorArgMarshalling = "new"; await runner.compile(` extern dec testDec(target: unknown, arg1: ${type}); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index ad5dd68d90..3bdba5db68 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -5,7 +5,7 @@ import { FunctionParameterNode, Model, Type, - defineModuleFlags, + definePackageFlags, } from "../../src/core/index.js"; import { BasicTestRunner, @@ -38,8 +38,8 @@ describe("compiler: checker: type relations", () => { expectedDiagnosticPos: number; }> { host.addJsFile("mock.js", { - $flags: defineModuleFlags({ - decoratorArgMarshalling: "lossless", + $flags: definePackageFlags({ + decoratorArgMarshalling: "new", }), $mock: () => null, }); diff --git a/packages/compiler/test/checker/values/utils.ts b/packages/compiler/test/checker/values/utils.ts index 619faeb657..c1c895ca66 100644 --- a/packages/compiler/test/checker/values/utils.ts +++ b/packages/compiler/test/checker/values/utils.ts @@ -1,5 +1,5 @@ import { ok } from "assert"; -import { Diagnostic, Model, Type, Value, defineModuleFlags } from "../../../src/index.js"; +import { Diagnostic, Model, Type, Value, definePackageFlags } from "../../../src/index.js"; import { createTestHost, createTestRunner, @@ -70,7 +70,7 @@ export async function compileAndDiagnoseValueOrType( const host = await createTestHost(); host.addJsFile("collect.js", { $collect: () => {}, - $flags: defineModuleFlags({ decoratorArgMarshalling: "lossless" }), + $flags: definePackageFlags({ decoratorArgMarshalling: "new" }), }); host.addTypeSpecFile( "main.tsp", diff --git a/packages/json-schema/src/lib.ts b/packages/json-schema/src/lib.ts index c11be6b0fe..5239c5e4fe 100644 --- a/packages/json-schema/src/lib.ts +++ b/packages/json-schema/src/lib.ts @@ -1,6 +1,6 @@ import { createTypeSpecLibrary, - defineModuleFlags, + definePackageFlags, JSONSchemaType, paramMessage, } from "@typespec/compiler"; @@ -112,8 +112,8 @@ export const $lib = createTypeSpecLibrary({ }, } as const); -export const $flags = defineModuleFlags({ - decoratorArgMarshalling: "lossless", +export const $flags = definePackageFlags({ + decoratorArgMarshalling: "new", }); export const { reportDiagnostic, createStateSymbol } = $lib; diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index 434e5858cd..c07e11e33c 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -1,4 +1,4 @@ -import { defineModuleFlags } from "@typespec/compiler"; +import { definePackageFlags } from "@typespec/compiler"; import { createTestHost, expectDiagnosticEmpty } from "@typespec/compiler/testing"; import { describe, expect, it } from "vitest"; import { generateExternDecorators } from "../../src/gen-extern-signatures/gen-extern-signatures.js"; @@ -13,8 +13,8 @@ async function generateDecoratorSignatures(code: string) { ${code}` ); host.addJsFile("lib.js", { - $flags: defineModuleFlags({ - decoratorArgMarshalling: "lossless", + $flags: definePackageFlags({ + decoratorArgMarshalling: "new", }), }); await host.diagnose("main.tsp", { From e8c4b7c8e0353df71d1f535ac8d2134865f6db77 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 10:47:08 -0700 Subject: [PATCH 158/184] Fix bug --- packages/compiler/src/core/checker.ts | 13 ++++++++++++- packages/compiler/src/core/messages.ts | 4 +++- .../compiler/test/checker/templates.test.ts | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c4de0c3e87..a652bcbb0b 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -716,7 +716,7 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic( createDiagnostic({ code: "value-in-type", - messageId: "templateConstraint", + messageId: "referenceTemplate", target: node, }) ); @@ -1522,6 +1522,17 @@ export function createChecker(program: Program): Checker { } else if (isErrorType(type)) { // If we got an error type we don't want to keep passing it through so we reduce to unknown // Similar to the above where if the type is not assignable to the constraint we reduce to the constraint + commit(param, unknownType); + continue; + } else if (isValue(type)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "value-in-type", + messageId: "noTemplateConstraint", + target: argNode, + }) + ); + commit(param, unknownType); continue; } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 244d411e5e..479b6ca1a4 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -448,7 +448,9 @@ const diagnostics = { severity: "error", messages: { default: "A value cannot be used as a type.", - templateConstraint: "Template parameter can be passed values but is used as a type.", + referenceTemplate: "Template parameter can be passed values but is used as a type.", + noTemplateConstraint: + "Template parameter has no constraint but a value is passed. Add `extends valueof unknown` to accept any value.", }, }, "no-prop": { diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index cef1cf0abc..8e50216dff 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -226,6 +226,24 @@ describe("compiler: templates", () => { }); }); + it("emits diagnostics when passing value to template parameter without constraint", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A { } + const a = "abc"; + alias B = A; + ` + ); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: + "Template parameter has no constraint but a value is passed. Add `extends valueof unknown` to accept any value.", + }); + }); + describe("instantiating a template with invalid args", () => { it("shouldn't pass thru the invalid args", async () => { const { pos, source } = extractCursor(` From 520d6816f3ecf10a140e120a465fb7a10178feb4 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 10:48:06 -0700 Subject: [PATCH 159/184] Rename --- .chronus/changes/feature-object-literals-2024-3-16-11-58-32.md | 2 +- .chronus/changes/feature-object-literals-32024-2-15-18-36-3.md | 2 +- packages/compiler/test/parser.test.ts | 2 +- packages/compiler/test/server/colorization.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md index 2bfb48a17b..3a883cf7a3 100644 --- a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md +++ b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md @@ -4,4 +4,4 @@ packages: - "@typespec/openapi3" --- -Add support for new object and tuple literals as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) +Add support for new object and array literals as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) diff --git a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md index c0edf6219b..4ae1576f01 100644 --- a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md +++ b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md @@ -5,4 +5,4 @@ packages: - "@typespec/http" --- -Update Flow Template to make use of the new tuple literals +Update Flow Template to make use of the new array literals diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index cd74413334..e9f2385114 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -261,7 +261,7 @@ describe("compiler: parser", () => { ]); }); - describe("tuple literals", () => { + describe("array literals", () => { parseEach([ `const A = #["abc"];`, `const A = #["abc", 123];`, diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 3c8d391aa2..41ff57d467 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -1199,7 +1199,7 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); - describe("tuple literals", () => { + describe("array literals", () => { it("empty", async () => { const tokens = await tokenizeWithConst("#[]"); deepStrictEqual(tokens, [ From f4de4343f0ebbaf18e728bac93455f75f8c473ed Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 10:54:38 -0700 Subject: [PATCH 160/184] Cleanup --- packages/compiler/src/core/checker.ts | 6 +++--- packages/compiler/src/core/projector.ts | 2 +- packages/compiler/src/core/types.ts | 12 +++++++++--- .../test/checker/values/string-values.test.ts | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index a652bcbb0b..b1d2633ea6 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3319,7 +3319,7 @@ export function createChecker(program: Program): Checker { } } - function createIndeterminateEntity(type: Type): IndeterminateEntity { + function createIndeterminateEntity(type: IndeterminateEntity["type"]): IndeterminateEntity { return { metaKind: "Indeterminate", type, @@ -4098,13 +4098,13 @@ export function createChecker(program: Program): Checker { if (argNode) { const arg = getValueForNode(argNode, mapper, { kind: "argument", - type: parameter.type as any, // TODO: change if we change this to not be a FunctionParameter + type: parameter.type, }); if (arg === null) { hasError = true; continue; } - if (checkValueOfType(arg, parameter.type as any, argNode)) { + if (checkValueOfType(arg, parameter.type, argNode)) { resolvedArgs.push(arg); } else { hasError = true; diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index ab16f73873..c1dc8d232b 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -110,7 +110,7 @@ export function createProjector( return type; } if ("metaKind" in type) { - return { metaKind: "Indeterminate", type: projectType(type.type) }; + return { metaKind: "Indeterminate", type: projectType(type.type) as any }; } if (projectedTypes.has(type)) { return projectedTypes.get(type)!; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 5cfdff7df9..c226f13eaa 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -185,7 +185,14 @@ export interface MixedParameterConstraint { /** When an entity that could be used as a type or value has not figured out if it is a value or type yet. */ export interface IndeterminateEntity { readonly metaKind: "Indeterminate"; - readonly type: Type; // TODO: better typeing? + readonly type: + | StringLiteral + | StringTemplate + | NumericLiteral + | BooleanLiteral + | EnumMember + | UnionVariant + | NullType; } export interface IntrinsicType extends BaseType { @@ -601,7 +608,7 @@ export interface StringLiteral extends BaseType { export interface NumericLiteral extends BaseType { kind: "Number"; node?: NumericLiteralNode; - value: number; // TODO: should we deprecate this? + value: number; numericValue: Numeric; valueAsString: string; } @@ -713,7 +720,6 @@ export interface FunctionParameterBase extends BaseType { /** Represent a function parameter that could accept types or values in the TypeSpec program. */ export interface MixedFunctionParameter extends FunctionParameterBase { - // TODO: better name? mixed: true; type: MixedParameterConstraint; } diff --git a/packages/compiler/test/checker/values/string-values.test.ts b/packages/compiler/test/checker/values/string-values.test.ts index 0ae55b3fc9..8c10f3103c 100644 --- a/packages/compiler/test/checker/values/string-values.test.ts +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -104,7 +104,7 @@ describe("validate literal are assignable", () => { `"abc"`, [ ["✔", `"abc"`], - // ["✔", `"a\${"b"}c"`], // TODO: should that work? it doesn't in main + ["✔", `"a\${"b"}c"`], [`✘`, `string("abc")`, `Type 'string' is not assignable to type '"abc"'`], ], ], From c30035069eae2a26b21bffa2723d8560daca4e7a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 12:40:45 -0700 Subject: [PATCH 161/184] rename tuple literal to array literal --- .../feature-object-literals-2024-2-15-18-36-3-1.md | 2 +- packages/compiler/src/core/checker.ts | 11 ++++++----- .../tuple-to-literal.codefix.ts | 4 ++-- packages/compiler/src/core/parser.ts | 14 +++++++------- packages/compiler/src/core/types.ts | 14 +++++++------- packages/compiler/src/formatter/print/printer.ts | 10 +++++----- packages/compiler/test/checker/decorators.test.ts | 4 ++-- packages/compiler/test/checker/relation.test.ts | 14 +++++++------- .../test/checker/values/array-values.test.ts | 2 +- .../tuple-to-literal.codefix.test.ts | 2 +- packages/spec/src/spec.emu.html | 4 ++-- 11 files changed, 41 insertions(+), 40 deletions(-) diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md index b8e9fe10e1..9a0c41d6b1 100644 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md @@ -5,7 +5,7 @@ packages: - "@typespec/compiler" --- -Using a tuple expression instead of a tuple literal when expecting a value is deprecated. A codefix will be provided to automatically convert tuple expressions into a literal. +Using a tuple expression instead of a array literal when expecting a value is deprecated. A codefix will be provided to automatically convert tuple expressions into a literal. ```tsp model Test { diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index b1d2633ea6..0a3e4e2f86 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -62,6 +62,7 @@ import { import { AliasStatementNode, ArrayExpressionNode, + ArrayLiteralNode, ArrayModelType, ArrayValue, AugmentDecoratorStatementNode, @@ -186,7 +187,6 @@ import { TemplatedType, Tuple, TupleExpressionNode, - TupleLiteralNode, Type, TypeInstantiationMap, TypeMapper, @@ -880,7 +880,8 @@ export function createChecker(program: Program): Checker { code: "deprecated", codefixes: [createTupleToLiteralCodeFix(tuple.node)], format: { - message: "Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", + message: + "Using a tuple as a value is deprecated. Use an array literal instead(with #[]).", }, target: tuple.node, }) @@ -1049,7 +1050,7 @@ export function createChecker(program: Program): Checker { return unknownType; case SyntaxKind.ObjectLiteral: return checkObjectValue(node, mapper, valueConstraint); - case SyntaxKind.TupleLiteral: + case SyntaxKind.ArrayLiteral: return checkArrayValue(node, mapper, valueConstraint); case SyntaxKind.ConstStatement: return checkConst(node); @@ -3799,7 +3800,7 @@ export function createChecker(program: Program): Checker { } function checkArrayValue( - node: TupleLiteralNode, + node: ArrayLiteralNode, mapper: TypeMapper | undefined, constraint: CheckValueConstraint | undefined ): ArrayValue | null { @@ -3828,7 +3829,7 @@ export function createChecker(program: Program): Checker { }; } - function createTypeForArrayValue(node: TupleLiteralNode, values: Value[]): Tuple { + function createTypeForArrayValue(node: ArrayLiteralNode, values: Value[]): Tuple { return createAndFinishType({ kind: "Tuple", node, diff --git a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts index bad5e26d8d..37995fa651 100644 --- a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts +++ b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts @@ -2,12 +2,12 @@ import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; import type { TupleExpressionNode } from "../types.js"; /** - * Quick fix that convert a tuple to a tuple literal. + * Quick fix that convert a tuple to an array literal. */ export function createTupleToLiteralCodeFix(node: TupleExpressionNode) { return defineCodeFix({ id: "tuple-to-literal", - label: `Convert to a tuple literal \`#[]\``, + label: `Convert to an array literal \`#[]\``, fix: (context) => { const location = getSourceLocation(node); return context.prependText(location, "#"); diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 174d39943d..0f18aa39be 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -16,6 +16,7 @@ import { import { AliasStatementNode, AnyKeywordNode, + ArrayLiteralNode, AugmentDecoratorStatementNode, BlockComment, BooleanLiteralNode, @@ -109,7 +110,6 @@ import { TemplateParameterDeclarationNode, TextRange, TupleExpressionNode, - TupleLiteralNode, TypeOfExpressionNode, TypeReferenceNode, TypeSpecScriptNode, @@ -290,7 +290,7 @@ namespace ListKind { close: Token.CloseBracket, } as const; - export const TupleLiteral = { + export const ArrayLiteral = { ...ExpresionsBase, allowEmpty: true, open: Token.HashBracket, @@ -1661,7 +1661,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.HashBrace: return parseObjectLiteral(); case Token.HashBracket: - return parseTupleLiteral(); + return parseArrayLiteral(); case Token.VoidKeyword: return parseVoidKeyword(); case Token.NeverKeyword: @@ -1751,11 +1751,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } - function parseTupleLiteral(): TupleLiteralNode { + function parseArrayLiteral(): ArrayLiteralNode { const pos = tokenPos(); - const values = parseList(ListKind.TupleLiteral, parseExpression); + const values = parseList(ListKind.ArrayLiteral, parseExpression); return { - kind: SyntaxKind.TupleLiteral, + kind: SyntaxKind.ArrayLiteral, values, ...finishNode(pos), }; @@ -3656,7 +3656,7 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined return visitNode(cb, node.id) || visitNode(cb, node.value); case SyntaxKind.ObjectLiteralSpreadProperty: return visitNode(cb, node.target); - case SyntaxKind.TupleLiteral: + case SyntaxKind.ArrayLiteral: return visitEach(cb, node.values); // no children for the rest of these. case SyntaxKind.StringTemplateHead: diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index c226f13eaa..dd92a9573b 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -382,7 +382,7 @@ export interface ObjectValuePropertyDescriptor { export interface ArrayValue extends BaseValue { valueKind: "ArrayValue"; - node: TupleLiteralNode; + node: ArrayLiteralNode; values: Value[]; } @@ -645,7 +645,7 @@ export interface StringTemplateSpanValue extends BaseType { export interface Tuple extends BaseType { kind: "Tuple"; - node: TupleExpressionNode | TupleLiteralNode; + node: TupleExpressionNode | ArrayLiteralNode; values: Type[]; } @@ -978,7 +978,7 @@ export enum SyntaxKind { ObjectLiteral, ObjectLiteralProperty, ObjectLiteralSpreadProperty, - TupleLiteral, + ArrayLiteral, ConstStatement, CallExpression, ScalarConstructor, @@ -1082,7 +1082,7 @@ export type Node = | ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode | ScalarConstructorNode - | TupleLiteralNode; + | ArrayLiteralNode; /** * Node that can be used as template @@ -1243,7 +1243,7 @@ export type Expression = | MemberExpressionNode | ModelExpressionNode | ObjectLiteralNode - | TupleLiteralNode + | ArrayLiteralNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode @@ -1475,8 +1475,8 @@ export interface ObjectLiteralSpreadPropertyNode extends BaseNode { readonly parent?: ObjectLiteralNode; } -export interface TupleLiteralNode extends BaseNode { - readonly kind: SyntaxKind.TupleLiteral; +export interface ArrayLiteralNode extends BaseNode { + readonly kind: SyntaxKind.ArrayLiteral; readonly values: readonly Expression[]; } diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 14ca7178e6..50efdb47c8 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -6,6 +6,7 @@ import { Keywords } from "../../core/scanner.js"; import { AliasStatementNode, ArrayExpressionNode, + ArrayLiteralNode, AugmentDecoratorStatementNode, BlockComment, BooleanLiteralNode, @@ -71,7 +72,6 @@ import { TemplateParameterDeclarationNode, TextRange, TupleExpressionNode, - TupleLiteralNode, TypeOfExpressionNode, TypeReferenceNode, TypeSpecScriptNode, @@ -390,8 +390,8 @@ export function printNode( options, print ); - case SyntaxKind.TupleLiteral: - return printTupleLiteral(path as AstPath, options, print); + case SyntaxKind.ArrayLiteral: + return printArrayLiteral(path as AstPath, options, print); case SyntaxKind.ConstStatement: return printConstStatement(path as AstPath, options, print); case SyntaxKind.CallExpression: @@ -1053,8 +1053,8 @@ export function printObjectLiteralSpreadProperty( return [printDirectives(path, options, print), "...", path.call(print, "target")]; } -export function printTupleLiteral( - path: AstPath, +export function printArrayLiteral( + path: AstPath, options: TypeSpecPrettierOptions, print: PrettierChildPrint ) { diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index e8cd4def16..a5cc5db671 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -442,7 +442,7 @@ describe("compiler: checker: decorators", () => { }); }); - describe("passing an tuple literal", () => { + describe("passing an array literal", () => { it("valueof model cast the value to a JS array", async () => { const arg = await testCallDecorator("valueof string[]", `#["foo"]`); deepStrictEqual(arg, ["foo"]); @@ -605,7 +605,7 @@ describe("compiler: checker: decorators", () => { }); }); - describe("passing an tuple literal", () => { + describe("passing an array literal", () => { it("valueof model cast the value to a JS array", async () => { const arg = await testCallDecorator("valueof string[]", `#["foo"]`); deepStrictEqual(arg, ["foo"]); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 3bdba5db68..41d9c713e3 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1477,7 +1477,7 @@ describe("compiler: checker: type relations", () => { }); }); - it("cannot assign a tuple literal", async () => { + it("cannot assign a array literal", async () => { await expectValueNotAssignableToConstraint( { source: `#["foo"]`, @@ -1503,14 +1503,14 @@ describe("compiler: checker: type relations", () => { }); describe("valueof array", () => { - it("can assign tuple literal", async () => { + it("can assign array literal", async () => { await expectValueAssignableToConstraint({ source: `#["foo"]`, target: "valueof string[]", }); }); - it("can assign tuple literal of object literal", async () => { + it("can assign array literal of object literal", async () => { await expectValueAssignableToConstraint({ source: `#[#{name: "a"}, #{name: "b"}]`, target: "valueof Info[]", @@ -1557,14 +1557,14 @@ describe("compiler: checker: type relations", () => { }); describe("valueof tuple", () => { - it("can assign tuple literal", async () => { + it("can assign array literal", async () => { await expectValueAssignableToConstraint({ source: `#["foo", 12]`, target: "valueof [string, int32]", }); }); - it("cannot assign tuple literal with too few values", async () => { + it("cannot assign array literal with too few values", async () => { await expectValueNotAssignableToConstraint( { source: `#["foo"]`, @@ -1577,7 +1577,7 @@ describe("compiler: checker: type relations", () => { ); }); - it("cannot assign tuple literal with too many values", async () => { + it("cannot assign array literal with too many values", async () => { await expectValueNotAssignableToConstraint( { source: `#["a", "b", "c"]`, @@ -1592,7 +1592,7 @@ describe("compiler: checker: type relations", () => { }); describe("valueof union", () => { - it("can assign tuple literal variant", async () => { + it("can assign array literal variant", async () => { await expectValueAssignableToConstraint({ source: `#["foo", 12]`, target: "valueof ([string, int32] | string | boolean)", diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index d7fc483196..692683ccdb 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -105,7 +105,7 @@ describe("(LEGACY) cast tuple to array value", () => { expectDiagnostics(diagnostics, { code: "deprecated", message: - "Deprecated: Using a tuple as a value is deprecated. Use a tuple literal instead(with #[]).", + "Deprecated: Using a tuple as a value is deprecated. Use a array literal instead(with #[]).", pos, }); }); diff --git a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts index f52be771b2..7feb3bc423 100644 --- a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts +++ b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts @@ -4,7 +4,7 @@ import { createTupleToLiteralCodeFix } from "../../../src/core/compiler-code-fix import { SyntaxKind } from "../../../src/index.js"; import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; -it("it change tuple to a tuple literal", async () => { +it("it change tuple to a array literal", async () => { await expectCodeFixOnAst( ` model Foo { diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 62ee5ed2f0..9e4dc311c1 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -500,7 +500,7 @@

Syntactic Grammar

CallOrReferenceExpression ParenthesizedExpression[?InParameter] ObjectLiteral - TupleLiteral + ArrayLiteral ModelExpression TupleExpression @@ -553,7 +553,7 @@

Syntactic Grammar

ObjectLiteralSpreadProperty : `...` ReferenceExpression -TupleLiteral : +ArrayLiteral : `#[` ExpressionList? `]` ModelExpression : From f5582f32d6deb1595dfd1d7c2891e00bf8c595f2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 12:44:08 -0700 Subject: [PATCH 162/184] Todo --- packages/compiler/src/core/checker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 0a3e4e2f86..e8d3bcdc17 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4126,7 +4126,6 @@ export function createChecker(program: Program): Checker { }; } - // TODO: should those be called eval? function checkCallExpression( node: CallExpressionNode, mapper: TypeMapper | undefined From a110a5d936da8c164b7037da5704299a2aa75b35 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 13:14:53 -0700 Subject: [PATCH 163/184] Apply suggestions from code review Co-authored-by: Brian Terlson --- .../changes/feature-object-literals-2024-2-15-18-36-3-1.md | 2 +- .../changes/feature-object-literals-2024-2-15-18-36-3-2.md | 2 +- .chronus/changes/feature-object-literals-2024-2-15-18-36-3.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md index 9a0c41d6b1..cc0ab5bb76 100644 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md @@ -5,7 +5,7 @@ packages: - "@typespec/compiler" --- -Using a tuple expression instead of a array literal when expecting a value is deprecated. A codefix will be provided to automatically convert tuple expressions into a literal. +Using a tuple type as a value is deprecated. Tuple types in contexts where values are expected must be updated to be array values instead. A codefix is provided to automatically convert tuple types into array values. ```tsp model Test { diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md index 82c656b20b..de3f507c16 100644 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md @@ -5,7 +5,7 @@ packages: - "@typespec/compiler" --- -Using a model expression instead of an object literal when expecting a value is deprecated. A codefix will be provided to automatically convert the model expression into a literal. +Using a model type as a value is deprecated. Model types in contexts where values are expected must be updated to be object values instead. A codefix is provided to automatically convert model types into object values. ```tsp model Test { diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md index a61db8af69..46b8e211dd 100644 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md @@ -5,9 +5,9 @@ packages: - "@typespec/compiler" --- -Values In TypeSpec [See docs](https://tspwebsitepr.z22.web.core.windows.net/prs/3022/docs/next/language-basics/values) +Add syntax for declaring values. [See docs](https://typespec.io/docs/language-basics/values). -Object and array literals +Object and array values ```tsp @dummy(#{ name: "John", From 540dd96bb0872bfaba0da45d8a96b892151e9a54 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 13:45:36 -0700 Subject: [PATCH 164/184] Rename object literals -> object values where it make sense --- .../feature-object-literals-2024-2-15-18-36-3-2.md | 2 +- docs/language-basics/values.md | 2 +- packages/compiler/src/core/checker.ts | 3 +-- .../compiler-code-fixes/model-to-literal.codefix.ts | 4 ++-- packages/compiler/src/core/messages.ts | 2 +- packages/compiler/test/checker/decorators.test.ts | 4 ++-- packages/compiler/test/checker/relation.test.ts | 10 +++++----- .../compiler/test/checker/values/object-values.test.ts | 2 +- .../model-to-literal.codefix.test.ts | 2 +- 9 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md index 82c656b20b..0da008b0da 100644 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md @@ -5,7 +5,7 @@ packages: - "@typespec/compiler" --- -Using a model expression instead of an object literal when expecting a value is deprecated. A codefix will be provided to automatically convert the model expression into a literal. +Using a model expression instead of an object value when expecting a value is deprecated. A codefix will be provided to automatically convert the model expression into a literal. ```tsp model Test { diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 17b539d663..1e7b4476aa 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -11,7 +11,7 @@ Values cannot be used as types, and types cannot be used as values, they are com ## Value kinds -There are four kinds of values: objects, arrays, scalars. and null. These values can be created with object literals, array literals, scalar literals and initializers, and the null literal respectively. Additionally, values can result from referencing enum members and union variants. +There are four kinds of values: objects, arrays, scalars. and null. These values can be created with object values, array literals, scalar literals and initializers, and the null literal respectively. Additionally, values can result from referencing enum members and union variants. ### Object values diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e8d3bcdc17..d3b293fe76 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -819,8 +819,7 @@ export function createChecker(program: Program): Checker { code: "deprecated", codefixes: [createModelToLiteralCodeFix(model.node)], format: { - message: - "Using a model as a value is deprecated. Use an object literal instead(with #{}).", + message: "Using a model as a value is deprecated. Use an object value instead(with #{}).", }, target: model.node, }) diff --git a/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts index d444614c32..92adecda8e 100644 --- a/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts +++ b/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts @@ -2,12 +2,12 @@ import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; import type { ModelExpressionNode } from "../types.js"; /** - * Quick fix that convert a model expression to an object literal. + * Quick fix that convert a model expression to an object value. */ export function createModelToLiteralCodeFix(node: ModelExpressionNode) { return defineCodeFix({ id: "model-to-literal", - label: `Convert to an object literal \`#{}\``, + label: `Convert to an object value \`#{}\``, fix: (context) => { const location = getSourceLocation(node); return context.prependText(location, "#"); diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 479b6ca1a4..0bf122480a 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -474,7 +474,7 @@ const diagnostics = { "unexpected-property": { severity: "error", messages: { - default: paramMessage`Object literal may only specify known properties, and '${"propertyName"}' does not exist in type '${"type"}'.`, + default: paramMessage`Object value may only specify known properties, and '${"propertyName"}' does not exist in type '${"type"}'.`, }, }, "extends-interface": { diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index a5cc5db671..d0ff0b6f24 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -427,7 +427,7 @@ describe("compiler: checker: decorators", () => { }); }); - describe("passing an object literal", () => { + describe("passing an object value", () => { it("valueof model cast the value to a JS object", async () => { const arg = await testCallDecorator("valueof {name: string}", `#{name: "foo"}`); deepStrictEqual(arg, { name: "foo" }); @@ -590,7 +590,7 @@ describe("compiler: checker: decorators", () => { }); }); - describe("passing an object literal", () => { + describe("passing an object value", () => { it("valueof model cast the value to a JS object", async () => { const arg = await testCallDecorator("valueof {name: string}", `#{name: "foo"}`); deepStrictEqual(arg, { name: "foo" }); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 41d9c713e3..dfb7a7c644 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1411,7 +1411,7 @@ describe("compiler: checker: type relations", () => { }); describe("valueof model", () => { - it("can assign object literal", async () => { + it("can assign object value", async () => { await expectValueAssignableToConstraint({ source: `#{name: "foo"}`, target: "valueof Info", @@ -1419,7 +1419,7 @@ describe("compiler: checker: type relations", () => { }); }); - it("can assign object literal with optional properties", async () => { + it("can assign object value with optional properties", async () => { await expectValueAssignableToConstraint({ source: `#{name: "foo"}`, target: "valueof Info", @@ -1427,7 +1427,7 @@ describe("compiler: checker: type relations", () => { }); }); - it("can assign object literal with additional properties", async () => { + it("can assign object value with additional properties", async () => { await expectValueAssignableToConstraint({ source: `#{age: 21, name: "foo"}`, target: "valueof Info", @@ -1510,7 +1510,7 @@ describe("compiler: checker: type relations", () => { }); }); - it("can assign array literal of object literal", async () => { + it("can assign array literal of object value", async () => { await expectValueAssignableToConstraint({ source: `#[#{name: "a"}, #{name: "b"}]`, target: "valueof Info[]", @@ -1532,7 +1532,7 @@ describe("compiler: checker: type relations", () => { ); }); - it("cannot assign an object literal", async () => { + it("cannot assign an object value", async () => { await expectValueNotAssignableToConstraint( { source: `#{name: "foo"}`, diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index 907f252a7f..33e5419f2a 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -171,7 +171,7 @@ describe("(LEGACY) cast model to object value", () => { expectDiagnostics(diagnostics, { code: "deprecated", message: - "Deprecated: Using a model as a value is deprecated. Use an object literal instead(with #{}).", + "Deprecated: Using a model as a value is deprecated. Use an object value instead(with #{}).", pos, }); }); diff --git a/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts index 19b772a859..dc664c18d9 100644 --- a/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts +++ b/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts @@ -4,7 +4,7 @@ import { createModelToLiteralCodeFix } from "../../../src/core/compiler-code-fix import { SyntaxKind } from "../../../src/index.js"; import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; -it("it change model expression to an object literal", async () => { +it("it change model expression to an object value", async () => { await expectCodeFixOnAst( ` model Foo { From 4086dafa62be1e4f5c85e26a4938d46a0a2f73f4 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 13:56:03 -0700 Subject: [PATCH 165/184] Migrate array literal to value nameing --- .../feature-object-literals-2024-3-16-11-58-32.md | 2 +- .../feature-object-literals-32024-2-15-18-36-3.md | 3 ++- docs/language-basics/values.md | 2 +- packages/compiler/src/core/checker.ts | 3 +-- .../tuple-to-literal.codefix.ts | 4 ++-- packages/compiler/test/checker/decorators.test.ts | 4 ++-- packages/compiler/test/checker/relation.test.ts | 14 +++++++------- .../test/checker/values/array-values.test.ts | 2 +- .../tuple-to-literal.codefix.test.ts | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md index 3a883cf7a3..3a6f6da209 100644 --- a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md +++ b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md @@ -4,4 +4,4 @@ packages: - "@typespec/openapi3" --- -Add support for new object and array literals as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) +Add support for new object and array values as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) diff --git a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md index 4ae1576f01..25e9849327 100644 --- a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md +++ b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md @@ -5,4 +5,5 @@ packages: - "@typespec/http" --- -Update Flow Template to make use of the new array literals +Update Flow Template to make use of the new array values + diff --git a/docs/language-basics/values.md b/docs/language-basics/values.md index 1e7b4476aa..4bed4774f0 100644 --- a/docs/language-basics/values.md +++ b/docs/language-basics/values.md @@ -11,7 +11,7 @@ Values cannot be used as types, and types cannot be used as values, they are com ## Value kinds -There are four kinds of values: objects, arrays, scalars. and null. These values can be created with object values, array literals, scalar literals and initializers, and the null literal respectively. Additionally, values can result from referencing enum members and union variants. +There are four kinds of values: objects, arrays, scalars. and null. These values can be created with object values, array values, scalar values and initializers, and the null literal respectively. Additionally, values can result from referencing enum members and union variants. ### Object values diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d3b293fe76..5921092178 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -879,8 +879,7 @@ export function createChecker(program: Program): Checker { code: "deprecated", codefixes: [createTupleToLiteralCodeFix(tuple.node)], format: { - message: - "Using a tuple as a value is deprecated. Use an array literal instead(with #[]).", + message: "Using a tuple as a value is deprecated. Use an array value instead(with #[]).", }, target: tuple.node, }) diff --git a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts index 37995fa651..2a51b5acb1 100644 --- a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts +++ b/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts @@ -2,12 +2,12 @@ import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; import type { TupleExpressionNode } from "../types.js"; /** - * Quick fix that convert a tuple to an array literal. + * Quick fix that convert a tuple to an array value. */ export function createTupleToLiteralCodeFix(node: TupleExpressionNode) { return defineCodeFix({ id: "tuple-to-literal", - label: `Convert to an array literal \`#[]\``, + label: `Convert to an array value \`#[]\``, fix: (context) => { const location = getSourceLocation(node); return context.prependText(location, "#"); diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index d0ff0b6f24..4b2a77d95f 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -442,7 +442,7 @@ describe("compiler: checker: decorators", () => { }); }); - describe("passing an array literal", () => { + describe("passing an array value", () => { it("valueof model cast the value to a JS array", async () => { const arg = await testCallDecorator("valueof string[]", `#["foo"]`); deepStrictEqual(arg, ["foo"]); @@ -605,7 +605,7 @@ describe("compiler: checker: decorators", () => { }); }); - describe("passing an array literal", () => { + describe("passing an array value", () => { it("valueof model cast the value to a JS array", async () => { const arg = await testCallDecorator("valueof string[]", `#["foo"]`); deepStrictEqual(arg, ["foo"]); diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index dfb7a7c644..70b6ef945d 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1477,7 +1477,7 @@ describe("compiler: checker: type relations", () => { }); }); - it("cannot assign a array literal", async () => { + it("cannot assign a array value", async () => { await expectValueNotAssignableToConstraint( { source: `#["foo"]`, @@ -1503,14 +1503,14 @@ describe("compiler: checker: type relations", () => { }); describe("valueof array", () => { - it("can assign array literal", async () => { + it("can assign array value", async () => { await expectValueAssignableToConstraint({ source: `#["foo"]`, target: "valueof string[]", }); }); - it("can assign array literal of object value", async () => { + it("can assign array value of object value", async () => { await expectValueAssignableToConstraint({ source: `#[#{name: "a"}, #{name: "b"}]`, target: "valueof Info[]", @@ -1557,14 +1557,14 @@ describe("compiler: checker: type relations", () => { }); describe("valueof tuple", () => { - it("can assign array literal", async () => { + it("can assign array value", async () => { await expectValueAssignableToConstraint({ source: `#["foo", 12]`, target: "valueof [string, int32]", }); }); - it("cannot assign array literal with too few values", async () => { + it("cannot assign array value with too few values", async () => { await expectValueNotAssignableToConstraint( { source: `#["foo"]`, @@ -1577,7 +1577,7 @@ describe("compiler: checker: type relations", () => { ); }); - it("cannot assign array literal with too many values", async () => { + it("cannot assign array value with too many values", async () => { await expectValueNotAssignableToConstraint( { source: `#["a", "b", "c"]`, @@ -1592,7 +1592,7 @@ describe("compiler: checker: type relations", () => { }); describe("valueof union", () => { - it("can assign array literal variant", async () => { + it("can assign array value variant", async () => { await expectValueAssignableToConstraint({ source: `#["foo", 12]`, target: "valueof ([string, int32] | string | boolean)", diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index 692683ccdb..90773e520c 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -105,7 +105,7 @@ describe("(LEGACY) cast tuple to array value", () => { expectDiagnostics(diagnostics, { code: "deprecated", message: - "Deprecated: Using a tuple as a value is deprecated. Use a array literal instead(with #[]).", + "Deprecated: Using a tuple as a value is deprecated. Use an array value instead(with #[]).", pos, }); }); diff --git a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts index 7feb3bc423..41ecd70406 100644 --- a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts +++ b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts @@ -4,7 +4,7 @@ import { createTupleToLiteralCodeFix } from "../../../src/core/compiler-code-fix import { SyntaxKind } from "../../../src/index.js"; import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; -it("it change tuple to a array literal", async () => { +it("it change tuple to a array value", async () => { await expectCodeFixOnAst( ` model Foo { From ef3b656734b8e59d7dae62faf69742642ff354fb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 13:56:31 -0700 Subject: [PATCH 166/184] Update .chronus/changes/feature-object-literals-2024-3-16-17-54-23.md Co-authored-by: Brian Terlson --- .chronus/changes/feature-object-literals-2024-3-16-17-54-23.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md b/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md index 7cb1e20f75..9855fea9fe 100644 --- a/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md +++ b/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md @@ -5,4 +5,4 @@ packages: - "@typespec/html-program-viewer" --- -Add support for new fields added with the value world +Add support for values From 158e991c048d4051b2096fc0c6cd4c403227033c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 13:56:38 -0700 Subject: [PATCH 167/184] Update .chronus/changes/feature-object-literals-2024-3-16-10-38-3.md Co-authored-by: Brian Terlson --- .chronus/changes/feature-object-literals-2024-3-16-10-38-3.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md index e20e1ae8b8..d288989ff9 100644 --- a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md +++ b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md @@ -8,7 +8,6 @@ Decorator API: Legacy marshalling logic If a library had a decorator with `valueof` one of those types `numeric`, `int64`, `uint64`, `integer`, `float`, `decimal`, `decimal128`, `null` it used to marshall those as JS `number` and `NullType` for `null`. With the introduction of values we have a new marshalling logic which will marshall those numeric types as `Numeric` and the others will remain numbers. `null` will also get marshalled as `null`. - For now this is an opt-in behavior with a warning on decorators not opt-in having a parameter with a constraint from the list above. Example: ```tsp From ed754c75046caede03839a29552d7b99015b8c3e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 13:56:43 -0700 Subject: [PATCH 168/184] Update .chronus/changes/feature-object-literals-2024-3-16-10-38-3.md Co-authored-by: Brian Terlson --- .chronus/changes/feature-object-literals-2024-3-16-10-38-3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md index d288989ff9..60dd5fe793 100644 --- a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md +++ b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md @@ -6,7 +6,7 @@ packages: Decorator API: Legacy marshalling logic - If a library had a decorator with `valueof` one of those types `numeric`, `int64`, `uint64`, `integer`, `float`, `decimal`, `decimal128`, `null` it used to marshall those as JS `number` and `NullType` for `null`. With the introduction of values we have a new marshalling logic which will marshall those numeric types as `Numeric` and the others will remain numbers. `null` will also get marshalled as `null`. +With the introduction of values, the decorator marshalling behavior has changed in some cases. This behavior is opt-in by setting the `valueMarshalling` package flag to `"new"`, but will be the default behavior in future versions. It is strongly recommended to adopt this new behavior as soon as possible. Example: From d1a0052b30c42d5bc3a2063d2200a273bed3c877 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 16:22:44 -0700 Subject: [PATCH 169/184] Update packages/compiler/lib/intrinsics.tsp Co-authored-by: Brian Terlson --- packages/compiler/lib/intrinsics.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/lib/intrinsics.tsp b/packages/compiler/lib/intrinsics.tsp index faa2348970..13faf93e96 100644 --- a/packages/compiler/lib/intrinsics.tsp +++ b/packages/compiler/lib/intrinsics.tsp @@ -110,7 +110,7 @@ scalar plainTime; */ scalar utcDateTime { /** - * Create a date from an ISO string. + * Create a date from an ISO 8601 string. */ init fromISO(value: string); } From 66dc669b13167175e4236d02ab1605f6b98398f4 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 16:23:38 -0700 Subject: [PATCH 170/184] Update packages/compiler/lib/intrinsics.tsp Co-authored-by: Brian Terlson --- packages/compiler/lib/intrinsics.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/lib/intrinsics.tsp b/packages/compiler/lib/intrinsics.tsp index 13faf93e96..295a6151d3 100644 --- a/packages/compiler/lib/intrinsics.tsp +++ b/packages/compiler/lib/intrinsics.tsp @@ -120,7 +120,7 @@ scalar utcDateTime { */ scalar offsetDateTime { /** - * Create a date from an ISO string. + * Create a date from an ISO 8601 string. */ init fromISO(value: string); } From 741e03d366a63192e48443e973d86a17fe3e561b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 16:26:28 -0700 Subject: [PATCH 171/184] Some CR fixes --- packages/compiler/lib/intrinsics.tsp | 7 ++++++- packages/compiler/src/core/checker.ts | 8 ++++---- ...eral.codefix.ts => model-to-object-literal.codefix.ts} | 4 ++-- ...literal.codefix.ts => tuple-to-array-value.codefix.ts} | 4 ++-- .../compiler-code-fixes/model-to-literal.codefix.test.ts | 4 ++-- .../compiler-code-fixes/tuple-to-literal.codefix.test.ts | 4 ++-- 6 files changed, 18 insertions(+), 13 deletions(-) rename packages/compiler/src/core/compiler-code-fixes/{model-to-literal.codefix.ts => model-to-object-literal.codefix.ts} (78%) rename packages/compiler/src/core/compiler-code-fixes/{tuple-to-literal.codefix.ts => tuple-to-array-value.codefix.ts} (78%) diff --git a/packages/compiler/lib/intrinsics.tsp b/packages/compiler/lib/intrinsics.tsp index 295a6151d3..75a0a22ffa 100644 --- a/packages/compiler/lib/intrinsics.tsp +++ b/packages/compiler/lib/intrinsics.tsp @@ -128,7 +128,12 @@ scalar offsetDateTime { /** * A duration/time period. e.g 5s, 10h */ -scalar duration; +scalar duration { + /** + * Create a date from an ISO 8601 string. + */ + init fromISO(value: string); +} /** * Boolean with `true` and `false` values. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5921092178..4b8e1f8097 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2,8 +2,8 @@ import { $docFromComment, getIndexer } from "../lib/intrinsic-decorators.js"; import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; import { createSymbol, createSymbolTable } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; -import { createModelToLiteralCodeFix } from "./compiler-code-fixes/model-to-literal.codefix.js"; -import { createTupleToLiteralCodeFix } from "./compiler-code-fixes/tuple-to-literal.codefix.js"; +import { createModelToObjectValueCodeFix } from "./compiler-code-fixes/model-to-object-literal.codefix.js"; +import { createTupleToArrayValueCodeFix } from "./compiler-code-fixes/tuple-to-array-value.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { ProjectionError, @@ -817,7 +817,7 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic( createDiagnostic({ code: "deprecated", - codefixes: [createModelToLiteralCodeFix(model.node)], + codefixes: [createModelToObjectValueCodeFix(model.node)], format: { message: "Using a model as a value is deprecated. Use an object value instead(with #{}).", }, @@ -877,7 +877,7 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic( createDiagnostic({ code: "deprecated", - codefixes: [createTupleToLiteralCodeFix(tuple.node)], + codefixes: [createTupleToArrayValueCodeFix(tuple.node)], format: { message: "Using a tuple as a value is deprecated. Use an array value instead(with #[]).", }, diff --git a/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/model-to-object-literal.codefix.ts similarity index 78% rename from packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts rename to packages/compiler/src/core/compiler-code-fixes/model-to-object-literal.codefix.ts index 92adecda8e..3b96de6154 100644 --- a/packages/compiler/src/core/compiler-code-fixes/model-to-literal.codefix.ts +++ b/packages/compiler/src/core/compiler-code-fixes/model-to-object-literal.codefix.ts @@ -4,9 +4,9 @@ import type { ModelExpressionNode } from "../types.js"; /** * Quick fix that convert a model expression to an object value. */ -export function createModelToLiteralCodeFix(node: ModelExpressionNode) { +export function createModelToObjectValueCodeFix(node: ModelExpressionNode) { return defineCodeFix({ - id: "model-to-literal", + id: "model-to-object-value", label: `Convert to an object value \`#{}\``, fix: (context) => { const location = getSourceLocation(node); diff --git a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/tuple-to-array-value.codefix.ts similarity index 78% rename from packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts rename to packages/compiler/src/core/compiler-code-fixes/tuple-to-array-value.codefix.ts index 2a51b5acb1..4a7855426d 100644 --- a/packages/compiler/src/core/compiler-code-fixes/tuple-to-literal.codefix.ts +++ b/packages/compiler/src/core/compiler-code-fixes/tuple-to-array-value.codefix.ts @@ -4,9 +4,9 @@ import type { TupleExpressionNode } from "../types.js"; /** * Quick fix that convert a tuple to an array value. */ -export function createTupleToLiteralCodeFix(node: TupleExpressionNode) { +export function createTupleToArrayValueCodeFix(node: TupleExpressionNode) { return defineCodeFix({ - id: "tuple-to-literal", + id: "tuple-to-array-value", label: `Convert to an array value \`#[]\``, fix: (context) => { const location = getSourceLocation(node); diff --git a/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts index dc664c18d9..7218f027d8 100644 --- a/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts +++ b/packages/compiler/test/core/compiler-code-fixes/model-to-literal.codefix.test.ts @@ -1,6 +1,6 @@ import { strictEqual } from "assert"; import { it } from "vitest"; -import { createModelToLiteralCodeFix } from "../../../src/core/compiler-code-fixes/model-to-literal.codefix.js"; +import { createModelToObjectValueCodeFix } from "../../../src/core/compiler-code-fixes/model-to-object-literal.codefix.js"; import { SyntaxKind } from "../../../src/index.js"; import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; @@ -13,7 +13,7 @@ it("it change model expression to an object value", async () => { `, (node) => { strictEqual(node.kind, SyntaxKind.ModelExpression); - return createModelToLiteralCodeFix(node); + return createModelToObjectValueCodeFix(node); } ).toChangeTo(` model Foo { diff --git a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts index 41ecd70406..89bd56fcbe 100644 --- a/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts +++ b/packages/compiler/test/core/compiler-code-fixes/tuple-to-literal.codefix.test.ts @@ -1,6 +1,6 @@ import { strictEqual } from "assert"; import { it } from "vitest"; -import { createTupleToLiteralCodeFix } from "../../../src/core/compiler-code-fixes/tuple-to-literal.codefix.js"; +import { createTupleToArrayValueCodeFix } from "../../../src/core/compiler-code-fixes/tuple-to-array-value.codefix.js"; import { SyntaxKind } from "../../../src/index.js"; import { expectCodeFixOnAst } from "../../../src/testing/code-fix-testing.js"; @@ -13,7 +13,7 @@ it("it change tuple to a array value", async () => { `, (node) => { strictEqual(node.kind, SyntaxKind.TupleExpression); - return createTupleToLiteralCodeFix(node); + return createTupleToArrayValueCodeFix(node); } ).toChangeTo(` model Foo { From fbab31ee561f2993b66978624bb2df35b9531ba3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 16:26:53 -0700 Subject: [PATCH 172/184] Update packages/compiler/src/core/messages.ts Co-authored-by: Brian Terlson --- packages/compiler/src/core/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 0bf122480a..230e82cf0a 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -144,7 +144,7 @@ const diagnostics = { statement: "Statement expected.", property: "Property expected.", enumMember: "Enum member expected.", - typeofTarget: "Typeof expect a literal or value reference.", + typeofTarget: "Typeof expects a value literal or value reference.", }, }, "trailing-token": { From 4d53ef4c90c052570aaa5dc7690b1156291aabba Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 16:31:11 -0700 Subject: [PATCH 173/184] More CR comments --- packages/compiler/src/core/checker.ts | 4 ++-- packages/compiler/src/core/library.ts | 4 ++-- packages/compiler/src/core/types.ts | 17 +++++++++-------- .../compiler/test/checker/decorators.test.ts | 4 ++-- packages/compiler/test/parser.test.ts | 2 +- packages/json-schema/lib/main.tsp | 1 - 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 4b8e1f8097..9ce6fd1451 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -160,7 +160,7 @@ import { ScalarConstructorNode, ScalarStatementNode, ScalarValue, - SigFunctionParameter, + SignatureFunctionParameter, StdTypeName, StdTypes, StringLiteral, @@ -2074,7 +2074,7 @@ export function createChecker(program: Program): Checker { node: FunctionParameterNode, mapper: TypeMapper | undefined, mixed: false - ): SigFunctionParameter; + ): SignatureFunctionParameter; function checkFunctionParameter( node: FunctionParameterNode, mapper: TypeMapper | undefined, diff --git a/packages/compiler/src/core/library.ts b/packages/compiler/src/core/library.ts index 255332beb4..27330ad23c 100644 --- a/packages/compiler/src/core/library.ts +++ b/packages/compiler/src/core/library.ts @@ -7,7 +7,7 @@ import { JSONSchemaValidator, LinterDefinition, LinterRuleDefinition, - ModuleFlags, + PackageFlags, StateDef, TypeSpecLibrary, TypeSpecLibraryDef, @@ -103,7 +103,7 @@ export function createTypeSpecLibrary< } } -export function definePackageFlags(flags: ModuleFlags): ModuleFlags { +export function definePackageFlags(flags: PackageFlags): PackageFlags { return flags; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index dd92a9573b..37b6df33bf 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -388,9 +388,10 @@ export interface ArrayValue extends BaseValue { export interface ScalarValue extends BaseValue { valueKind: "ScalarValue"; - scalar: Scalar; // We need to keep a reference of what scalar this is. - value: { name: string; args: Value[] }; // e.g. for utcDateTime(2020,12,01) + scalar: Scalar; + value: { name: string; args: Value[] }; } + export interface NumericValue extends BaseValue { valueKind: "NumericValue"; scalar: Scalar | undefined; @@ -449,7 +450,7 @@ export interface ScalarConstructor extends BaseType { node: ScalarConstructorNode; name: string; scalar: Scalar; - parameters: SigFunctionParameter[]; + parameters: SignatureFunctionParameter[]; } export interface Interface extends BaseType, DecoratedType, TemplatedTypeBase { @@ -724,11 +725,11 @@ export interface MixedFunctionParameter extends FunctionParameterBase { type: MixedParameterConstraint; } /** Represent a function parameter that represent the parameter signature(i.e the type would be the type of the value passed) */ -export interface SigFunctionParameter extends FunctionParameterBase { +export interface SignatureFunctionParameter extends FunctionParameterBase { mixed: false; type: Type; } -export type FunctionParameter = MixedFunctionParameter | SigFunctionParameter; +export type FunctionParameter = MixedFunctionParameter | SignatureFunctionParameter; export interface Sym { readonly flags: SymbolFlags; @@ -1993,7 +1994,7 @@ export type LocationContext = /** Defined in the user project. */ export interface ProjectLocationContext { readonly type: "project"; - readonly flags?: ModuleFlags; + readonly flags?: PackageFlags; } /** Built-in */ @@ -2014,7 +2015,7 @@ export interface LibraryLocationContext { readonly metadata: ModuleLibraryMetadata; /** Module definition */ - readonly flags?: ModuleFlags; + readonly flags?: PackageFlags; } export interface LibraryInstance { @@ -2388,7 +2389,7 @@ export interface TypeSpecLibraryDef< readonly state?: Record; } -export interface ModuleFlags { +export interface PackageFlags { /** * Decorator arg marshalling algorithm. Specify how TypeSpec values are marshalled to decorator arguments. * - `lossless` - New recommended behavior diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 4b2a77d95f..ecf58ed479 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { ModuleFlags, isNullType, setTypeSpecNamespace } from "../../src/core/index.js"; +import { PackageFlags, isNullType, setTypeSpecNamespace } from "../../src/core/index.js"; import { numericRanges } from "../../src/core/numeric-ranges.js"; import { Numeric } from "../../src/core/numeric.js"; import { @@ -121,7 +121,7 @@ describe("compiler: checker: decorators", () => { describe("usage", () => { let runner: BasicTestRunner; let calledArgs: any[] | undefined; - let $flags: ModuleFlags; + let $flags: PackageFlags; beforeEach(() => { $flags = {}; calledArgs = undefined; diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index e9f2385114..d65cb17f0d 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -281,7 +281,7 @@ describe("compiler: parser", () => { describe("typeof expressions", () => { parseEach([`const a: typeof "123" = 123;`, `alias A = Foo;`]); parseErrorEach([ - [`alias A = typeof #{}`, [{ message: "Typeof expect a literal or value reference." }]], + [`alias A = typeof #{}`, [{ message: "Typeof expects a value literal or value reference." }]], ]); }); diff --git a/packages/json-schema/lib/main.tsp b/packages/json-schema/lib/main.tsp index 78ea6ef8f6..fae17c91d9 100644 --- a/packages/json-schema/lib/main.tsp +++ b/packages/json-schema/lib/main.tsp @@ -35,7 +35,6 @@ extern dec id(target: unknown, id: valueof string); * * @param value The numeric type must be a multiple of this value. */ -#suppress "deprecated" "" extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); /** From 94ce40a3f589cc42aafdb01ee0ac2cb7fa967b59 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 6 May 2024 17:22:02 -0700 Subject: [PATCH 174/184] ADd more scalar constructor and docs --- packages/compiler/lib/intrinsics.tsp | 41 ++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/compiler/lib/intrinsics.tsp b/packages/compiler/lib/intrinsics.tsp index 75a0a22ffa..6ad6745981 100644 --- a/packages/compiler/lib/intrinsics.tsp +++ b/packages/compiler/lib/intrinsics.tsp @@ -98,12 +98,32 @@ scalar string; /** * A date on a calendar without a time zone, e.g. "April 10th" */ -scalar plainDate; +scalar plainDate { + /** + * Create a plain date from an ISO 8601 string. + * @example + * + * ```tsp + * const time = plainTime.fromISO("2024-05-06"); + * ``` + */ + init fromISO(value: string); +} /** * A time on a clock without a time zone, e.g. "3:00 am" */ -scalar plainTime; +scalar plainTime { + /** + * Create a plain time from an ISO 8601 string. + * @example + * + * ```tsp + * const time = plainTime.fromISO("12:34"); + * ``` + */ + init fromISO(value: string); +} /** * An instant in coordinated universal time (UTC)" @@ -111,6 +131,11 @@ scalar plainTime; scalar utcDateTime { /** * Create a date from an ISO 8601 string. + * @example + * + * ```tsp + * const time = utcDateTime.fromISO("2024-05-06T12:20-12Z"); + * ``` */ init fromISO(value: string); } @@ -121,6 +146,11 @@ scalar utcDateTime { scalar offsetDateTime { /** * Create a date from an ISO 8601 string. + * @example + * + * ```tsp + * const time = offsetDateTime.fromISO("2024-05-06T12:20-12-0700"); + * ``` */ init fromISO(value: string); } @@ -130,7 +160,12 @@ scalar offsetDateTime { */ scalar duration { /** - * Create a date from an ISO 8601 string. + * Create a duration from an ISO 8601 string. + * @example + * + * ```tsp + * const time = duration.fromISO("P1Y1D"); + * ``` */ init fromISO(value: string); } From 9a0298040b884b7e55e2aa7f3d870732f9451689 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 7 May 2024 09:04:22 -0700 Subject: [PATCH 175/184] Add tests --- packages/compiler/src/core/checker.ts | 11 ++++++++++- packages/compiler/test/checker/relation.test.ts | 2 +- .../compiler/test/checker/valueof-casting.test.ts | 10 ++++++++++ packages/compiler/test/decorators/decorators.test.ts | 5 ----- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 6980bf8acf..5383b3da3e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -959,7 +959,16 @@ export function createChecker(program: Program): Checker { compilerAssert(entity.metaKind === "Indeterminate", "Expected indeterminate entity"); if (valueConstraint) { - return getValueFromIndeterminate(entity.type, valueConstraint, node); + const valueDiagnostics: Diagnostic[] = []; + const oldDiagnosticHook = onCheckerDiagnostic; + onCheckerDiagnostic = (x: Diagnostic) => valueDiagnostics.push(x); + const result = getValueFromIndeterminate(entity.type, valueConstraint, node); + onCheckerDiagnostic = oldDiagnosticHook; + if (result) { + // If there were diagnostic reported but we still got a value this means that the value might be invalid. + reportCheckerDiagnostics(valueDiagnostics); + return result; + } } return entity.type; diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 14943026e8..336fcd1952 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -905,7 +905,7 @@ describe("compiler: checker: type relations", () => { { source: `{foo?: string}`, target: `{foo: string}` }, { code: "property-required", - message: "Property 'foo' is required in type '(anonymous model)' but here is optional.", + message: "Property 'foo' is required in type '{ foo: string }' but here is optional.", } ); }); diff --git a/packages/compiler/test/checker/valueof-casting.test.ts b/packages/compiler/test/checker/valueof-casting.test.ts index fff3063f08..b4a4218e05 100644 --- a/packages/compiler/test/checker/valueof-casting.test.ts +++ b/packages/compiler/test/checker/valueof-casting.test.ts @@ -42,3 +42,13 @@ it("ambiguous valueof with type option still emit ambiguous error", async () => "Value 123 type is ambiguous between int32, int64. To resolve be explicit when instantiating this value(e.g. 'int32(123)').", }); }); + +it("passing an enum member to 'EnumMember | valueof string' pass the type", async () => { + const entity = await compileValueOrType( + "Reflection.EnumMember | valueof string", + `A.a`, + `enum A { a }` + ); + ok(isType(entity)); + strictEqual(entity.kind, "EnumMember"); +}); diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index e12a82530e..d9fedf691f 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -222,7 +222,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -267,7 +266,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); @@ -320,7 +318,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -347,7 +344,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument of type '123' is not assignable to parameter of type 'string'`, }); }); }); @@ -520,7 +516,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument of type '4' is not assignable to parameter of type 'string'", }, ]); }); From 5eca49f8f9e61c9b40461d1633d898f07cc47f23 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 7 May 2024 09:23:42 -0700 Subject: [PATCH 176/184] Fix marshalling of enum members --- packages/compiler/src/core/checker.ts | 38 ++++++++++++--------- packages/compiler/src/core/js-marshaller.ts | 2 +- packages/compiler/src/core/types.ts | 11 +++++- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5383b3da3e..c5a321cf48 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1997,33 +1997,37 @@ export function createChecker(program: Program): Checker { function checkDecoratorLegacyMarshalling(decorator: Decorator) { const marshalling = resolveDecoratorArgMarshalling(decorator); + function reportDeprecatedLegacyMarshalling(param: MixedFunctionParameter, message: string) { + reportDeprecated( + program, + [ + `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting ${message}.`, + `This will change in the future.`, + 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library.', + ].join("\n"), + param.node + ); + } if (marshalling === "legacy") { for (const param of decorator.parameters) { if (param.type.valueType) { if (ignoreDiagnostics(isTypeAssignableTo(nullType, param.type.valueType, param.type))) { - reportDeprecated( - program, - [ - `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting null as a type.`, - `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library.', - ].join("\n"), - param.node - ); + reportDeprecatedLegacyMarshalling(param, "null as a type"); + } else if ( + param.type.valueType.kind === "Enum" || + param.type.valueType.kind === "EnumMember" || + (isReflectionType(param.type.valueType) && param.type.valueType.name === "EnumMember") + ) { + reportDeprecatedLegacyMarshalling(param, "enum members"); } else if ( ignoreDiagnostics( isTypeAssignableTo(param.type.valueType, getStdType("numeric"), param.type.valueType) ) && !canNumericConstraintBeJsNumber(param.type.valueType) ) { - reportDeprecated( - program, - [ - `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting a numeric type that is not representable as a JS Number.`, - `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library.', - ].join("\n"), - param.node + reportDeprecatedLegacyMarshalling( + param, + "a numeric type that is not representable as a JS Number" ); } } diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 03889b7acc..2d16f1bbd1 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -56,7 +56,7 @@ export function marshallTypeForJS( case "ArrayValue": return arrayValueToJs(value) as any; case "EnumValue": - return value.value as any; + return value as any; case "NullValue": return null as any; case "ScalarValue": diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 37b6df33bf..027e120173 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -365,7 +365,16 @@ export type Value = interface BaseValue { valueKind: string; - type: Type; // Every value has a type. That type could be something completely different(much wider type) + /** + * Represent the storage type of a value. + * @example + * ```tsp + * const a = "hello"; // Type here would be "hello" + * const b: string = a; // Type here would be string + * const c: string | int32 = b; // Type here would be string | int32 + * ``` + */ + type: Type; } export interface ObjectValue extends BaseValue { From 1b5948477f27e78a7d9a6700892d105a7e5eeaed Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 7 May 2024 09:37:57 -0700 Subject: [PATCH 177/184] Fix another issue with template default --- packages/compiler/src/core/checker.ts | 19 +++++++++++-------- .../compiler/test/checker/templates.test.ts | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c5a321cf48..62777c6dfb 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1187,15 +1187,18 @@ export function createChecker(program: Program): Checker { declaredType: TemplateParameter, node: TemplateParameterDeclarationNode, mapper: TypeMapper - ): Type | undefined { + ): Type | Value | IndeterminateEntity | null | undefined { if (declaredType.default === undefined) { return undefined; } - if (isType(declaredType.default) && isErrorType(declaredType.default)) { + if ( + (isType(declaredType.default) && isErrorType(declaredType.default)) || + declaredType.default === null + ) { return declaredType.default; } - return getTypeForNode(node.default!, mapper); + return checkNode(node.default!, mapper); } function checkTemplateParameterDefault( @@ -1205,18 +1208,18 @@ export function createChecker(program: Program): Checker { constraint: Entity | undefined ): Type | Value | IndeterminateEntity { function visit(node: Node) { - const type = checkNode(node); + const entity = checkNode(node); let hasError = false; - if (type !== null && "kind" in type && type.kind === "TemplateParameter") { + if (entity !== null && "kind" in entity && entity.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { - if (type.node.symbol === templateParameters[i].symbol) { + if (entity.node.symbol === templateParameters[i].symbol) { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-template-default", target: node }) ); return undefined; } } - return type; + return entity; } visitChildren(node, (x) => { @@ -1226,7 +1229,7 @@ export function createChecker(program: Program): Checker { } }); - return hasError ? undefined : type; + return hasError ? undefined : entity; } const type = visit(nodeDefault) ?? errorType; diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 8e50216dff..571c29e86d 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -8,6 +8,7 @@ import { TestHost, createTestHost, createTestRunner, + expectDiagnosticEmpty, expectDiagnostics, extractCursor, extractSquiggles, @@ -112,6 +113,22 @@ describe("compiler: templates", () => { strictEqual((b.type as StringLiteral).value, "hi"); }); + it("indeterminate defaults", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model B {} + @test model A { + b: B + } + alias Test = A; + ` + ); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnosticEmpty(diagnostics); + }); + it("allows default template parameters that are models", async () => { testHost.addTypeSpecFile( "main.tsp", From 6030a9c3500324c2b23a169184080d5d071e4f2d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 7 May 2024 12:03:13 -0700 Subject: [PATCH 178/184] Update packages/compiler/src/core/checker.ts Co-authored-by: Brian Terlson --- packages/compiler/src/core/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 62777c6dfb..8799c1f072 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2006,7 +2006,7 @@ export function createChecker(program: Program): Checker { [ `Parameter ${param.name} of decorator ${decorator.name} is using legacy marshalling but is accepting ${message}.`, `This will change in the future.`, - 'To opt-in today add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library.', + 'Add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library to opt-in to the new marshalling behavior.', ].join("\n"), param.node ); From feecb80e819ec20a0affdfd165ea5b11707703cd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 7 May 2024 12:03:20 -0700 Subject: [PATCH 179/184] Update packages/compiler/src/core/checker.ts Co-authored-by: Brian Terlson --- packages/compiler/src/core/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 8799c1f072..b61de5b2e9 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5363,7 +5363,7 @@ export function createChecker(program: Program): Checker { const name = node.id.sv; const links = getSymbolLinksForMember(node); if (links && links.declaredType && mapper === undefined) { - // we're not instantiating this union variant and we've already checked it + // we're not instantiating this scalar constructor and we've already checked it return links.declaredType as ScalarConstructor; } From 83f613d0b445994aa36528d0ed198c70061e19e7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 8 May 2024 09:17:17 -0700 Subject: [PATCH 180/184] Add new `entityKind` property to --- packages/compiler/src/core/checker.ts | 79 +++++++++++-------- packages/compiler/src/core/diagnostics.ts | 2 +- .../src/core/helpers/type-name-utils.ts | 2 +- packages/compiler/src/core/program.ts | 2 +- packages/compiler/src/core/projector.ts | 4 +- packages/compiler/src/core/type-utils.ts | 4 +- packages/compiler/src/core/types.ts | 8 +- .../src/emitter-framework/type-emitter.ts | 2 +- .../test/checker/augment-decorators.test.ts | 1 + packages/html-program-viewer/src/ui.tsx | 1 + .../decorators-signatures.ts | 2 +- 11 files changed, 61 insertions(+), 46 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index b61de5b2e9..ee52ad2f6d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -204,7 +204,7 @@ import { VoidType, } from "./types.js"; -export type CreateTypeProps = Omit; +export type CreateTypeProps = Omit; export interface Checker { typePrototype: TypePrototype; @@ -242,7 +242,7 @@ export interface Checker { resolveCompletions(node: IdentifierNode): Map; createType( typeDef: T - ): T & TypePrototype & { isFinished: boolean }; + ): T & TypePrototype & { isFinished: boolean; readonly entityKind: "Type" }; createAndFinishType( typeDef: T ): T & TypePrototype; @@ -698,7 +698,7 @@ export function createChecker(program: Program): Checker { if (entity === null) { return errorType; } - if ("metaKind" in entity) { + if (entity.entityKind === "Indeterminate") { return entity.type; } if (isValue(entity)) { @@ -736,7 +736,7 @@ export function createChecker(program: Program): Checker { return null; } let entity: Type | Value | null; - if ("metaKind" in initial) { + if (initial.entityKind === "Indeterminate") { entity = getValueFromIndeterminate(initial.type, constraint, node); } else { entity = initial; @@ -826,6 +826,7 @@ export function createChecker(program: Program): Checker { ); const value: ObjectValue = { + entityKind: "Value", valueKind: "ObjectValue", type: type ?? model, node: model.node as any, @@ -918,6 +919,7 @@ export function createChecker(program: Program): Checker { } return { + entityKind: "Value", valueKind: "ArrayValue", type: type ?? tuple, node: tuple.node as any, @@ -956,7 +958,7 @@ export function createChecker(program: Program): Checker { } else if (isValue(entity)) { return entity; } - compilerAssert(entity.metaKind === "Indeterminate", "Expected indeterminate entity"); + compilerAssert(entity.entityKind === "Indeterminate", "Expected indeterminate entity"); if (valueConstraint) { const valueDiagnostics: Diagnostic[] = []; @@ -1572,7 +1574,7 @@ export function createChecker(program: Program): Checker { reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); return errorType; } - if ("metaKind" in result) { + if (result.entityKind === "Indeterminate") { return result.type; } return result; @@ -1856,7 +1858,7 @@ export function createChecker(program: Program): Checker { } } return { - metaKind: "MixedParameterConstraint", + entityKind: "MixedParameterConstraint", node, valueType: values.length === 0 @@ -2129,7 +2131,7 @@ export function createChecker(program: Program): Checker { const type = node.type ? getParamConstraintEntityForNode(node.type) : ({ - metaKind: "MixedParameterConstraint", + entityKind: "MixedParameterConstraint", type: unknownType, } satisfies MixedParameterConstraint); parameterType = createType({ @@ -2172,7 +2174,7 @@ export function createChecker(program: Program): Checker { default: const [kind, entity] = getTypeOrValueOfTypeForNode(node, mapper); return { - metaKind: "MixedParameterConstraint", + entityKind: "MixedParameterConstraint", node: node, type: kind === "value" ? undefined : entity, valueType: kind === "value" ? entity : undefined, @@ -3310,7 +3312,7 @@ export function createChecker(program: Program): Checker { for (const [span, typeOrValue] of spanTypeOrValues) { compilerAssert(typeOrValue !== null && !isValue(typeOrValue), "Expected type."); - const type = "metaKind" in typeOrValue ? typeOrValue.type : typeOrValue; + const type = typeOrValue.entityKind === "Indeterminate" ? typeOrValue.type : typeOrValue; const spanValue = createTemplateSpanValue(span.expression, type); spans.push(spanValue); const spanValueAsString = stringifyTypeForTemplate(type); @@ -3336,7 +3338,7 @@ export function createChecker(program: Program): Checker { function createIndeterminateEntity(type: IndeterminateEntity["type"]): IndeterminateEntity { return { - metaKind: "Indeterminate", + entityKind: "Indeterminate", type, }; } @@ -3394,21 +3396,21 @@ export function createChecker(program: Program): Checker { function checkStringLiteral(str: StringLiteralNode): IndeterminateEntity { return { - metaKind: "Indeterminate", + entityKind: "Indeterminate", type: getLiteralType(str), }; } function checkNumericLiteral(num: NumericLiteralNode): IndeterminateEntity { return { - metaKind: "Indeterminate", + entityKind: "Indeterminate", type: getLiteralType(num), }; } function checkBooleanLiteral(bool: BooleanLiteralNode): IndeterminateEntity { return { - metaKind: "Indeterminate", + entityKind: "Indeterminate", type: getLiteralType(bool), }; } @@ -3596,7 +3598,9 @@ export function createChecker(program: Program): Checker { // Some of the mapper args are still template parameter so we shouldn't create the type. return ( !mapper.partial && - mapper.args.every((t) => isValue(t) || "metaKind" in t || t.kind !== "TemplateParameter") + mapper.args.every( + (t) => isValue(t) || t.entityKind === "Indeterminate" || t.kind !== "TemplateParameter" + ) ); } @@ -3731,6 +3735,7 @@ export function createChecker(program: Program): Checker { return null; } return { + entityKind: "Value", valueKind: "ObjectValue", node: node, properties, @@ -3836,6 +3841,7 @@ export function createChecker(program: Program): Checker { } return { + entityKind: "Value", valueKind: "ArrayValue", node: node, values: values as any, @@ -3914,6 +3920,7 @@ export function createChecker(program: Program): Checker { } const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); return { + entityKind: "Value", valueKind: "StringValue", value, type: constraint ? constraint.type : literalType, @@ -3931,6 +3938,7 @@ export function createChecker(program: Program): Checker { } const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); return { + entityKind: "Value", valueKind: "NumericValue", value: Numeric(literalType.valueAsString), type: constraint ? constraint.type : literalType, @@ -3948,6 +3956,7 @@ export function createChecker(program: Program): Checker { } const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); return { + entityKind: "Value", valueKind: "BooleanValue", value: literalType.value, type: constraint ? constraint.type : literalType, @@ -3965,6 +3974,8 @@ export function createChecker(program: Program): Checker { } return { + entityKind: "Value", + valueKind: "NullValue", type: constraint ? constraint.type : literalType, value: null, @@ -3980,6 +3991,8 @@ export function createChecker(program: Program): Checker { return null; } return { + entityKind: "Value", + valueKind: "EnumValue", type: constraint ? constraint.type : literalType, value: literalType, @@ -4130,6 +4143,7 @@ export function createChecker(program: Program): Checker { return null; } return { + entityKind: "Value", valueKind: "ScalarValue", value: { name: declaration.name, @@ -4176,7 +4190,7 @@ export function createChecker(program: Program): Checker { // Shouldn't need to emit error as we assume null value already emitted error when produced return errorType; } - if ("metaKind" in entity) { + if (entity.entityKind === "Indeterminate") { return entity.type; } @@ -4889,7 +4903,7 @@ export function createChecker(program: Program): Checker { if (resolved === null || isValue(resolved)) { return undefined; } - if ("metaKind" in resolved) { + if (resolved.entityKind === "Indeterminate") { return resolved.type; } return resolved; @@ -5009,7 +5023,7 @@ export function createChecker(program: Program): Checker { false, node.arguments.map((argNode): DecoratorArgument => { let type = checkNode(argNode, mapper) ?? errorType; - if ("metaKind" in type) { + if (type.entityKind === "Indeterminate") { type = type.type; } return { @@ -5148,7 +5162,7 @@ export function createChecker(program: Program): Checker { } return { - metaKind: "MixedParameterConstraint", + entityKind: "MixedParameterConstraint", type, valueType, }; @@ -5864,7 +5878,7 @@ export function createChecker(program: Program): Checker { function createAndFinishType( typeDef: T - ): T & TypePrototype & { isFinished: boolean } { + ): T & TypePrototype & { isFinished: boolean; readonly entityKind: "Type" } { createType(typeDef); return finishType(typeDef as any) as any; } @@ -5876,17 +5890,17 @@ export function createChecker(program: Program): Checker { */ function createType( typeDef: T - ): T & TypePrototype & { isFinished: boolean } { + ): T & TypePrototype & { isFinished: boolean; entityKind: "Type" } { Object.setPrototypeOf(typeDef, typePrototype); (typeDef as any).isFinished = false; // If the type has an associated syntax node, check any directives that // might be attached. const createdType = typeDef as any; + createdType.entityKind = "Type"; if (createdType.node) { checkDirectives(createdType.node, createdType); } - return createdType; } @@ -7039,11 +7053,11 @@ export function createChecker(program: Program): Checker { // BACKCOMPAT: Allow certain type to be accepted as values if ( "kind" in source && - "metaKind" in target && + "entityKind" in target && source.kind === "TemplateParameter" && source.constraint?.type && source.constraint.valueType === undefined && - target.metaKind === "MixedParameterConstraint" && + target.entityKind === "MixedParameterConstraint" && target.valueType ) { const [assignable] = isTypeAssignableToInternal( @@ -7068,7 +7082,7 @@ export function createChecker(program: Program): Checker { if ("kind" in source && source.kind === "TemplateParameter") { source = source.constraint ?? unknownType; } - if ("metaKind" in target && target.metaKind === "Indeterminate") { + if (target.entityKind === "Indeterminate") { target = target.type; } @@ -7077,11 +7091,11 @@ export function createChecker(program: Program): Checker { if (isValue(target)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - if ("metaKind" in source && source.metaKind === "Indeterminate") { + if (source.entityKind === "Indeterminate") { return isIndeterminateEntityAssignableTo(source, target, diagnosticTarget, relationCache); } - if ("metaKind" in target) { + if (target.entityKind === "MixedParameterConstraint") { return isAssignableToMixedParameterConstraint( source, target, @@ -7090,13 +7104,10 @@ export function createChecker(program: Program): Checker { ); } - if ( - isValue(source) || - ("metaKind" in source && source.metaKind === "MixedParameterConstraint" && source.valueType) - ) { + if (isValue(source) || (source.entityKind === "MixedParameterConstraint" && source.valueType)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - if ("metaKind" in source) { + if (source.entityKind === "MixedParameterConstraint") { return isTypeAssignableToInternal(source.type!, target, diagnosticTarget, relationCache); } @@ -7194,7 +7205,7 @@ export function createChecker(program: Program): Checker { return [Related.true, []]; } - if ("metaKind" in target && target.valueType) { + if (target.entityKind === "MixedParameterConstraint" && target.valueType) { const [valueRelated] = isTypeAssignableToInternal( indeterminate.type, target.valueType, @@ -7229,7 +7240,7 @@ export function createChecker(program: Program): Checker { diagnosticTarget: DiagnosticTarget, relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if ("metaKind" in source && source.metaKind === "MixedParameterConstraint") { + if ("entityKind" in source && source.entityKind === "MixedParameterConstraint") { if (source.type && target.type) { const [variantAssignable, diagnostics] = isTypeAssignableToInternal( source.type, diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index ce722b15aa..7e1eee9a26 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -83,7 +83,7 @@ export function getSourceLocation( return target; } - if (!("kind" in target) && !("valueKind" in target) && !("metaKind" in target)) { + if (!("kind" in target) && !("valueKind" in target) && !("entityKind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { target = target.symbolSource!; diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 46d95bd856..88088dcb07 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -90,7 +90,7 @@ export function getEntityName(entity: Entity, options?: TypeNameOptions): string } else if (isType(entity)) { return getTypeName(entity, options); } else { - switch (entity.metaKind) { + switch (entity.entityKind) { case "MixedParameterConstraint": return [ entity.type && getEntityName(entity.type), diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 2666eb1972..de70505b8a 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -1174,7 +1174,7 @@ export async function compile( } function getNode(target: Node | Entity | Sym): Node | undefined { - if (!("kind" in target) && !("valueKind" in target) && !("metaKind" in target)) { + if (!("kind" in target) && !("valueKind" in target) && !("entityKind" in target)) { // symbol if (target.flags & SymbolFlags.Using) { return target.symbolSource!.declarations[0]; diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index c1dc8d232b..7b1770482b 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -109,8 +109,8 @@ export function createProjector( if (isValue(type)) { return type; } - if ("metaKind" in type) { - return { metaKind: "Indeterminate", type: projectType(type.type) as any }; + if (type.entityKind === "Indeterminate") { + return { entityKind: "Indeterminate", type: projectType(type.type) as any }; } if (projectedTypes.has(type)) { return projectedTypes.get(type)!; diff --git a/packages/compiler/src/core/type-utils.ts b/packages/compiler/src/core/type-utils.ts index df9d87cb42..29a3c82f56 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -44,10 +44,10 @@ export function isNullType(type: Entity): type is NullType { } export function isType(entity: Entity): entity is Type { - return "kind" in entity; + return entity.entityKind === "Type"; } export function isValue(entity: Entity): entity is Value { - return "valueKind" in entity; + return entity.entityKind === "Value"; } /** diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 027e120173..120346e334 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -56,6 +56,7 @@ export interface DecoratorFunction { } export interface BaseType { + readonly entityKind: "Type"; kind: string; node?: Node; instantiationParameters?: Type[]; @@ -172,7 +173,7 @@ export interface Projector { } export interface MixedParameterConstraint { - readonly metaKind: "MixedParameterConstraint"; + readonly entityKind: "MixedParameterConstraint"; readonly node?: UnionExpressionNode | Expression; /** Type constraints */ @@ -184,7 +185,7 @@ export interface MixedParameterConstraint { /** When an entity that could be used as a type or value has not figured out if it is a value or type yet. */ export interface IndeterminateEntity { - readonly metaKind: "Indeterminate"; + readonly entityKind: "Indeterminate"; readonly type: | StringLiteral | StringTemplate @@ -364,7 +365,8 @@ export type Value = | NullValue; interface BaseValue { - valueKind: string; + readonly entityKind: "Value"; + readonly valueKind: string; /** * Represent the storage type of a value. * @example diff --git a/packages/compiler/src/emitter-framework/type-emitter.ts b/packages/compiler/src/emitter-framework/type-emitter.ts index 84d9317b3d..dbabf65a26 100644 --- a/packages/compiler/src/emitter-framework/type-emitter.ts +++ b/packages/compiler/src/emitter-framework/type-emitter.ts @@ -780,7 +780,7 @@ export class TypeEmitter> { let unspeakable = false; const parameterNames = declarationType.templateMapper.args.map((t) => { - if ("metaKind" in t) { + if (t.entityKind === "Indeterminate") { t = t.type; } if (!("kind" in t)) { diff --git a/packages/compiler/test/checker/augment-decorators.test.ts b/packages/compiler/test/checker/augment-decorators.test.ts index abfc3ef874..db12c279a9 100644 --- a/packages/compiler/test/checker/augment-decorators.test.ts +++ b/packages/compiler/test/checker/augment-decorators.test.ts @@ -236,6 +236,7 @@ describe("compiler: checker: augment decorators", () => { const stringTest = results.stringTest as Operation; strictEqual(stringTest.kind, "Operation"); deepEqual((stringTest.returnType as Model).decorators[0].args[0].value, { + entityKind: "Type", kind: "String", value: "Some foo thing", isFinished: false, diff --git a/packages/html-program-viewer/src/ui.tsx b/packages/html-program-viewer/src/ui.tsx index d1ee562405..fe027f72a8 100644 --- a/packages/html-program-viewer/src/ui.tsx +++ b/packages/html-program-viewer/src/ui.tsx @@ -94,6 +94,7 @@ export const ItemList = (props: ItemListProps) => { type NamedType = Type & { name: string }; const omittedProps = [ + "entityKind", "kind", "name", "node", diff --git a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts index a76c542d69..04458b306f 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -139,7 +139,7 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } return { - metaKind: "MixedParameterConstraint", + entityKind: "MixedParameterConstraint", type, valueType, }; From 3031a776667988c12ac1635d3c0457fad19a6909 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 8 May 2024 09:18:37 -0700 Subject: [PATCH 181/184] remove todo --- packages/compiler/src/core/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 120346e334..133681deed 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1361,7 +1361,6 @@ export interface ScalarStatementNode extends BaseNode, DeclarationNode, Template readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } -// TODO: should this be ScalarConstructorDeclarationNode? export interface ScalarConstructorNode extends BaseNode { readonly kind: SyntaxKind.ScalarConstructor; readonly id: IdentifierNode; From 2d068c7e13771718dde7dc44220e474baf13ab0f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 8 May 2024 09:41:11 -0700 Subject: [PATCH 182/184] Better errors when trying to get a value of model/tuple --- packages/compiler/src/core/checker.ts | 53 ++++++++++++++++++++------ packages/compiler/src/core/messages.ts | 2 + 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index ee52ad2f6d..e2880e4d6f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -750,16 +750,42 @@ export function createChecker(program: Program): Checker { if (isValue(entity)) { return constraint ? inferScalarsFromConstraints(entity, constraint.type) : entity; } - reportCheckerDiagnostic( - createDiagnostic({ - code: "expect-value", - format: { name: getTypeName(entity) }, - target: node, - }) - ); + reportExpectedValue(node, entity); return null; } + function reportExpectedValue(target: Node, type: Type) { + if (type.kind === "Model" && type.name === "" && target.kind === SyntaxKind.ModelExpression) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + messageId: "model", + format: { name: getTypeName(type) }, + codefixes: [createModelToObjectValueCodeFix(target)], + target, + }) + ); + } else if (type.kind === "Tuple" && target.kind === SyntaxKind.TupleExpression) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + messageId: "tuple", + format: { name: getTypeName(type) }, + codefixes: [createTupleToArrayValueCodeFix(target)], + target, + }) + ); + } else { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(type) }, + target, + }) + ); + } + } + /** In certain context for types that can also be value if the constraint allows it we try to use it as a value instead of a type. */ function getValueFromIndeterminate( type: Type, @@ -3730,6 +3756,9 @@ export function createChecker(program: Program): Checker { constraint: CheckValueConstraint | undefined ): ObjectValue | null { const properties = checkObjectLiteralProperties(node, mapper); + if (properties === null) { + return null; + } const preciseType = createTypeForObjectValue(node, properties); if (constraint && !checkTypeOfValueMatchConstraint(preciseType, constraint, node)) { return null; @@ -3781,13 +3810,15 @@ export function createChecker(program: Program): Checker { function checkObjectLiteralProperties( node: ObjectLiteralNode, mapper: TypeMapper | undefined - ): Map { + ): Map | null { const properties = new Map(); - + let hasError = false; for (const prop of node.properties!) { if ("id" in prop) { const value = getValueForNode(prop.value, mapper); - if (value !== null) { + if (value === null) { + hasError = true; + } else { properties.set(prop.id.sv, { name: prop.id.sv, value: value, node: prop }); } } else { @@ -3799,7 +3830,7 @@ export function createChecker(program: Program): Checker { } } } - return properties; + return hasError ? null : properties; } function checkObjectSpreadProperty( diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 9b3b439985..442a6f9833 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -409,6 +409,8 @@ const diagnostics = { severity: "error", messages: { default: paramMessage`${"name"} refers to a type, but is being used as a value here.`, + model: paramMessage`${"name"} refers to a model type, but is being used as a value here. Use #{} to create an object value.`, + tuple: paramMessage`${"name"} refers to 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.`, }, }, From 87bdb56267336008b2e74e98b147ca1c1ac3d694 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 8 May 2024 10:01:28 -0700 Subject: [PATCH 183/184] Fix tm grammar for scalar body --- grammars/typespec.json | 2 +- packages/compiler/src/server/tmlanguage.ts | 2 +- .../compiler/test/server/colorization.test.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/grammars/typespec.json b/grammars/typespec.json index a913cb0379..3929a1558e 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -1163,7 +1163,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index 0ad09f21f3..37a1ebe181 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -618,7 +618,7 @@ const scalarStatement: BeginEndRule = { "1": { scope: "keyword.other.tsp" }, "2": { scope: "entity.name.type.tsp" }, }, - end: universalEnd, + end: `(?<=\\})|${universalEnd}`, patterns: [ token, typeParameters, diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 41ff57d467..05a1173b7b 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -786,6 +786,22 @@ function testColorization(description: string, tokenize: Tokenize) { Token.punctuation.closeBrace, ]); }); + + it("scalar with body doesn't need semi colon for next statement", async () => { + const tokens = await tokenize(` + scalar foo { } + scalar bar; + `); + deepStrictEqual(tokens, [ + Token.keywords.scalar, + Token.identifiers.type("foo"), + Token.punctuation.openBrace, + Token.punctuation.closeBrace, + Token.keywords.scalar, + Token.identifiers.type("bar"), + Token.punctuation.semicolon, + ]); + }); }); it("named template argument list", async () => { From 2425dc6b4e146e5572c37fed1eab018af8e22fc1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 8 May 2024 10:06:23 -0700 Subject: [PATCH 184/184] Fix --- packages/compiler/test/checker/augment-decorators.test.ts | 1 + packages/compiler/test/checker/values/array-values.test.ts | 3 ++- packages/compiler/test/checker/values/object-values.test.ts | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/checker/augment-decorators.test.ts b/packages/compiler/test/checker/augment-decorators.test.ts index db12c279a9..05ef006362 100644 --- a/packages/compiler/test/checker/augment-decorators.test.ts +++ b/packages/compiler/test/checker/augment-decorators.test.ts @@ -243,6 +243,7 @@ describe("compiler: checker: augment decorators", () => { }); for (const prop of (stringTest.returnType as Model).properties) { deepEqual(prop[1].decorators[0].args[0].value, { + entityKind: "Type", kind: "String", value: "Some test prop", isFinished: false, diff --git a/packages/compiler/test/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts index 90773e520c..ebdb75147e 100644 --- a/packages/compiler/test/checker/values/array-values.test.ts +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -54,7 +54,8 @@ it("emit diagnostic if referencing a non literal type", async () => { const diagnostics = await diagnoseValue(`#[{ thisIsAModel: true }]`); expectDiagnostics(diagnostics, { code: "expect-value", - message: "{ thisIsAModel: true } refers to a type, but is being used as a value here.", + message: + "{ thisIsAModel: true } refers to a model type, but is being used as a value here. Use #{} to create an object value.", }); }); diff --git a/packages/compiler/test/checker/values/object-values.test.ts b/packages/compiler/test/checker/values/object-values.test.ts index 33e5419f2a..2cfa75562d 100644 --- a/packages/compiler/test/checker/values/object-values.test.ts +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -113,7 +113,8 @@ it("emit diagnostic if referencing a non literal type", async () => { const diagnostics = await diagnoseValue(`#{ prop: { thisIsAModel: true }}`); expectDiagnostics(diagnostics, { code: "expect-value", - message: "{ thisIsAModel: true } refers to a type, but is being used as a value here.", + message: + "{ thisIsAModel: true } refers to a model type, but is being used as a value here. Use #{} to create an object value.", }); });