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..cc0ab5bb76 --- /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 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 { + // 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..de3f507c16 --- /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 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 { + // 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 new file mode 100644 index 0000000000..46b8e211dd --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md @@ -0,0 +1,30 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Add syntax for declaring values. [See docs](https://typespec.io/docs/language-basics/values). + +Object and array values +```tsp +@dummy(#{ + name: "John", + age: 48, + address: #{ city: "London" } + 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-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 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..60dd5fe793 --- /dev/null +++ b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md @@ -0,0 +1,23 @@ +--- +changeKind: deprecation +packages: + - "@typespec/compiler" +--- + +Decorator API: Legacy marshalling logic + +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: + ```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 = definePackageFlags({ + decoratorArgMarshalling: "new", + }); + ``` 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..3a6f6da209 --- /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 array values 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/.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..9855fea9fe --- /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 values 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..25e9849327 --- /dev/null +++ b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md @@ -0,0 +1,9 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/http" +--- + +Update Flow Template to make use of the new array values + 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..857b93de8d 100644 --- a/docs/language-basics/templates.md +++ b/docs/language-basics/templates.md @@ -108,3 +108,43 @@ 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 = { + @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 new file mode 100644 index 0000000000..4bed4774f0 --- /dev/null +++ b/docs/language-basics/values.md @@ -0,0 +1,187 @@ +--- +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 or used as default values. + +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)). + +## Value kinds + +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 + +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 2 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(value: uint32); +} + +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. + +```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 {} +``` diff --git a/docs/libraries/http/reference/decorators.md b/docs/libraries/http/reference/decorators.md index 2996c91707..24c42c8d45 100644 --- a/docs/libraries/http/reference/decorators.md +++ b/docs/libraries/http/reference/decorators.md @@ -358,7 +358,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/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/grammars/typespec.json b/grammars/typespec.json index c2ab257ac0..3929a1558e 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -11,30 +11,45 @@ } ], "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" } ] }, "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,9 +77,62 @@ "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|`(?:[^`\\\\]|\\\\.)*`)", + "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)", + "begin": "((@)\\b[_$[:alpha:]](?:[_$[:alnum:]]|\\.[_$[:alpha:]])*\\b)", "beginCaptures": { "1": { "name": "entity.name.tag.tsp" @@ -328,15 +396,27 @@ { "include": "#valueof" }, + { + "include": "#typeof" + }, { "include": "#type-arguments" }, + { + "include": "#object-literal" + }, + { + "include": "#tuple-literal" + }, { "include": "#tuple-expression" }, { "include": "#model-expression" }, + { + "include": "#callExpression" + }, { "include": "#identifier-expression" } @@ -693,6 +773,59 @@ "name": "constant.numeric.tsp", "match": "(?:\\b(?)|(?=,|;|@|\\)|\\}|\\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/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/lib/intrinsics.tsp b/packages/compiler/lib/intrinsics.tsp index 0a7775ab26..6ad6745981 100644 --- a/packages/compiler/lib/intrinsics.tsp +++ b/packages/compiler/lib/intrinsics.tsp @@ -98,27 +98,77 @@ 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)" */ -scalar utcDateTime; +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); +} /** * 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 8601 string. + * @example + * + * ```tsp + * const time = offsetDateTime.fromISO("2024-05-06T12:20-12-0700"); + * ``` + */ + init fromISO(value: string); +} /** * A duration/time period. e.g 5s, 10h */ -scalar duration; +scalar duration { + /** + * Create a duration from an ISO 8601 string. + * @example + * + * ```tsp + * const time = duration.fromISO("P1Y1D"); + * ``` + */ + init fromISO(value: string); +} /** * Boolean with `true` and `false` values. diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index bfbbe8df5c..eb36ca53d4 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, @@ -133,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)) { @@ -271,6 +276,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 +481,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 +614,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 9af59b7d52..e2880e4d6f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,42 +1,77 @@ -import { $docFromComment, 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"; +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, 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 { isStringTemplateSerializable } from "./helpers/string-template-utils.js"; +import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; +import { + getMaxItems, + getMaxLength, + getMaxValueAsNumeric, + getMaxValueExclusiveAsNumeric, + getMinItems, + getMinLength, + getMinValueAsNumeric, + getMinValueExclusiveAsNumeric, +} from "./intrinsic-type-state.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"; import { exprIsBareIdentifier, getIdentifierContext, 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, + isNullType, isTemplateInstance, + isType, isUnknownType, + isValue, isVoidType, } from "./type-utils.js"; import { AliasStatementNode, ArrayExpressionNode, + ArrayLiteralNode, + ArrayModelType, + ArrayValue, AugmentDecoratorStatementNode, BooleanLiteral, BooleanLiteralNode, + BooleanValue, + CallExpressionNode, CodeFix, + ConstStatementNode, DecoratedType, Decorator, DecoratorApplication, @@ -47,10 +82,12 @@ import { Diagnostic, DiagnosticTarget, DocContent, + Entity, Enum, EnumMember, EnumMemberNode, EnumStatementNode, + EnumValue, ErrorType, Expression, FunctionDeclarationStatementNode, @@ -59,6 +96,7 @@ import { FunctionType, IdentifierKind, IdentifierNode, + IndeterminateEntity, Interface, InterfaceStatementNode, IntersectionExpressionNode, @@ -67,12 +105,13 @@ import { JsSourceFileNode, LiteralNode, LiteralType, - MarshalledValue, MemberContainerNode, MemberContainerType, MemberExpressionNode, MemberNode, MemberType, + MixedFunctionParameter, + MixedParameterConstraint, Model, ModelExpressionNode, ModelIndexer, @@ -85,8 +124,14 @@ import { NeverType, Node, NodeFlags, + NullType, + NullValue, NumericLiteral, NumericLiteralNode, + NumericValue, + ObjectLiteralNode, + ObjectValue, + ObjectValuePropertyDescriptor, Operation, OperationStatementNode, Projection, @@ -111,7 +156,11 @@ import { ReturnExpressionNode, ReturnRecord, Scalar, + ScalarConstructor, + ScalarConstructorNode, ScalarStatementNode, + ScalarValue, + SignatureFunctionParameter, StdTypeName, StdTypes, StringLiteral, @@ -124,6 +173,7 @@ import { StringTemplateSpanLiteral, StringTemplateSpanValue, StringTemplateTailNode, + StringValue, Sym, SymbolFlags, SymbolLinks, @@ -140,6 +190,7 @@ import { Type, TypeInstantiationMap, TypeMapper, + TypeOfExpressionNode, TypeOrReturnRecord, TypeReferenceNode, TypeSpecScriptNode, @@ -149,12 +200,11 @@ import { UnionVariant, UnionVariantNode, UnknownType, - ValueOfExpressionNode, - ValueType, + Value, VoidType, } from "./types.js"; -export type CreateTypeProps = Omit; +export type CreateTypeProps = Omit; export interface Checker { typePrototype: TypePrototype; @@ -192,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; @@ -218,8 +268,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[]]; @@ -247,10 +297,17 @@ export interface Checker { */ resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]]; - errorType: ErrorType; - voidType: VoidType; - neverType: NeverType; - anyType: UnknownType; + /** @internal */ + getValueForNode(node: Node): Value | null; + + /** @internal */ + getTypeOrValueForNode(node: Node): Type | Value | null; + + readonly errorType: ErrorType; + readonly voidType: VoidType; + readonly neverType: NeverType; + readonly nullType: NullType; + readonly anyType: UnknownType; } interface TypePrototype { @@ -387,6 +444,7 @@ export function createChecker(program: Program): Checker { project, neverType, errorType, + nullType, anyType: unknownType, voidType, typePrototype, @@ -399,6 +457,8 @@ export function createChecker(program: Program): Checker { isStdType, getStdType, resolveTypeReference, + getValueForNode, + getTypeOrValueForNode, }; const projectionMembers = createProjectionMembers(checker); @@ -510,7 +570,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); @@ -628,10 +688,341 @@ 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); } } function getTypeForNode(node: Node, mapper?: TypeMapper): Type { + const entity = checkNode(node, mapper); + if (entity === null) { + return errorType; + } + if (entity.entityKind === "Indeterminate") { + return entity.type; + } + if (isValue(entity)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "value-in-type", + target: node, + }) + ); + return errorType; + } + if (entity.kind === "TemplateParameter") { + if (entity.constraint?.valueType) { + // means this template constraint will accept values + reportCheckerDiagnostic( + createDiagnostic({ + code: "value-in-type", + messageId: "referenceTemplate", + target: node, + }) + ); + } + } + return entity; + } + + function getValueForNode( + node: Node, + mapper?: TypeMapper, + constraint?: CheckValueConstraint, + options: { legacyTupleAndModelCast?: boolean } = {} + ): Value | null { + const initial = checkNode(node, mapper, constraint); + if (initial === null) { + return null; + } + let entity: Type | Value | null; + if (initial.entityKind === "Indeterminate") { + entity = getValueFromIndeterminate(initial.type, constraint, node); + } else { + entity = initial; + } + if (options.legacyTupleAndModelCast && entity !== null && isType(entity)) { + entity = legacy_tryTypeToValueCast(entity, constraint, node); + } + if (entity === null) { + return null; + } + if (isValue(entity)) { + return constraint ? inferScalarsFromConstraints(entity, constraint.type) : entity; + } + 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, + constraint: CheckValueConstraint | undefined, + node: Node + ): Type | Value | null { + switch (type.kind) { + case "String": + case "StringTemplate": + return checkStringValue(type, constraint, node); + case "Number": + return checkNumericValue(type, constraint, node); + case "Boolean": + return checkBooleanValue(type, constraint, node); + case "EnumMember": + return checkEnumValue(type, constraint, node); + case "UnionVariant": + return getValueFromIndeterminate(type.type, constraint, node); + case "Intrinsic": + switch (type.name) { + case "null": + return checkNullValue(type as any, constraint, node); + } + return type; + default: + return type; + } + } + + 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, + 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: [createModelToObjectValueCodeFix(model.node)], + format: { + message: "Using a model as a value is deprecated. Use an object value instead(with #{}).", + }, + target: model.node, + }) + ); + + const value: ObjectValue = { + entityKind: "Value", + valueKind: "ObjectValue", + type: type ?? model, + node: model.node as any, + properties: new Map(), + }; + + for (const prop of model.properties.values()) { + let propValue = getValueFromIndeterminate( + 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)) { + 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 + ): 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: [createTupleToArrayValueCodeFix(tuple.node)], + format: { + message: "Using a tuple as a value is deprecated. Use an array value 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; + let value = getValueFromIndeterminate( + item, + itemType && { kind: "assignment", type: itemType }, + 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)) { + return tuple; + } + values.push(value); + } + + if (type !== undefined && !checkTypeAssignable(tuple, type, node)) { + return null; + } + + return { + entityKind: "Value", + valueKind: "ArrayValue", + type: type ?? tuple, + node: tuple.node as any, + values, + }; + } + + interface CheckConstraint { + kind: "argument" | "assignment"; + constraint: MixedParameterConstraint; + } + 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. + * 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?: CheckConstraint | undefined + ): Type | Value | null { + const valueConstraint = extractValueOfConstraints(constraint); + const entity = checkNode(node, mapper, valueConstraint); + 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.entityKind === "Indeterminate", "Expected indeterminate entity"); + + if (valueConstraint) { + 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; + } + + /** Extact the type constraint a value should match. */ + function extractValueOfConstraints( + constraint: CheckConstraint | undefined + ): CheckValueConstraint | undefined { + if (constraint?.constraint.valueType) { + return { kind: constraint.kind, type: constraint.constraint.valueType }; + } else { + return undefined; + } + } + + /** + * 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 + ): Type | Value | IndeterminateEntity | null { switch (node.kind) { case SyntaxKind.ModelExpression: return checkModel(node, mapper); @@ -662,10 +1053,10 @@ export function createChecker(program: Program): Checker { return checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: return checkBooleanLiteral(node); - case SyntaxKind.TupleExpression: - return checkTupleExpression(node, mapper); case SyntaxKind.StringLiteral: return checkStringLiteral(node); + case SyntaxKind.TupleExpression: + return checkTupleExpression(node, mapper); case SyntaxKind.StringTemplateExpression: return checkStringTemplateExpresion(node, mapper); case SyntaxKind.ArrayExpression: @@ -679,7 +1070,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: @@ -692,13 +1083,19 @@ export function createChecker(program: Program): Checker { return neverType; case SyntaxKind.UnknownKeyword: return unknownType; + case SyntaxKind.ObjectLiteral: + return checkObjectValue(node, mapper, valueConstraint); + case SyntaxKind.ArrayLiteral: + return checkArrayValue(node, mapper, valueConstraint); + case SyntaxKind.ConstStatement: + return checkConst(node); + case SyntaxKind.CallExpression: + return checkCallExpression(node, mapper); + case SyntaxKind.TypeOfExpression: + return checkTypeOfExpression(node, mapper); + 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; } /** @@ -709,6 +1106,7 @@ export function createChecker(program: Program): Checker { | ModelStatementNode | ScalarStatementNode | AliasStatementNode + | ConstStatementNode | InterfaceStatementNode | OperationStatementNode | TemplateParameterDeclarationNode @@ -743,10 +1141,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 | IndeterminateEntity; function checkTemplateParameterDeclaration( node: TemplateParameterDeclarationNode, mapper: TypeMapper | undefined - ): Type { + ): Type | Value | IndeterminateEntity; + function checkTemplateParameterDeclaration( + node: TemplateParameterDeclarationNode, + mapper: TypeMapper | undefined + ): Type | Value | IndeterminateEntity { const parentNode = node.parent!; const grandParentNode = parentNode.parent; const links = getSymbolLinks(node.symbol); @@ -785,7 +1195,7 @@ export function createChecker(program: Program): Checker { if (node.constraint) { pendingResolutions.start(getNodeSymId(node), ResolutionKind.Constraint); - type.constraint = getTypeOrValueTypeForNode(node.constraint); + type.constraint = getParamConstraintEntityForNode(node.constraint); pendingResolutions.finish(getNodeSymId(node), ResolutionKind.Constraint); } if (node.default) { @@ -805,36 +1215,39 @@ 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 (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( nodeDefault: Expression, templateParameters: readonly TemplateParameterDeclarationNode[], index: number, - constraint: Type | ValueType | undefined - ) { + constraint: Entity | undefined + ): Type | Value | IndeterminateEntity { function visit(node: Node) { - const type = getTypeForNode(node); + const entity = checkNode(node); let hasError = false; - if (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) => { @@ -844,11 +1257,11 @@ export function createChecker(program: Program): Checker { } }); - return hasError ? undefined : type; + return hasError ? undefined : entity; } const type = visit(nodeDefault) ?? errorType; - if (!isErrorType(type) && constraint) { + if (!("kind" in type && isErrorType(type)) && constraint) { checkTypeAssignable(type, constraint, nodeDefault); } return type; @@ -875,8 +1288,31 @@ export function createChecker(program: Program): Checker { return type; } - function checkTemplateArgument(node: TemplateArgumentNode, mapper: TypeMapper | undefined): Type { - return getTypeForNode(node.argument, mapper); + /** + * 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 | IndeterminateEntity { + const sym = resolveTypeReferenceSym(node, mapper); + if (!sym) { + return errorType; + } + + return checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplate) ?? errorType; + } + + function checkTemplateArgument( + node: TemplateArgumentNode, + mapper: TypeMapper | undefined + ): Type | Value | IndeterminateEntity | null { + return checkNode(node.argument, mapper); } function resolveTypeReference( @@ -959,17 +1395,17 @@ 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 | IndeterminateEntity | null]) | null; } const initMap = new Map( - decls.map(function (decl) { - const declaredType = getTypeForNode(decl)! as TemplateParameter; + decls.map((decl) => { + const declaredType = checkTemplateParameterDeclaration(decl, undefined); positional.push(declaredType); params.set(decl.id.sv, declaredType); @@ -987,8 +1423,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(): [Node, Type] { - return [arg, getTypeForNode(arg.argument, mapper)]; + function deferredCheck(): [Node, Type | Value | IndeterminateEntity | null] { + return [arg, checkNode(arg.argument, mapper)]; } if (arg.name) { @@ -1054,11 +1490,14 @@ export function createChecker(program: Program): Checker { } } - const finalMap = initMap as unknown as Map; + const finalMap = initMap as unknown as Map< + TemplateParameter, + Type | Value | IndeterminateEntity + >; const mapperParams: TemplateParameter[] = []; - const mapperArgs: Type[] = []; + const mapperArgs: (Type | Value | IndeterminateEntity)[] = []; for (const [param, { decl, checkArgument: init }] of [...initMap]) { - function commit(param: TemplateParameter, type: Type): void { + function commit(param: TemplateParameter, type: Type | Value | IndeterminateEntity): void { finalMap.set(param, type); mapperParams.push(param); mapperArgs.push(type); @@ -1082,26 +1521,39 @@ export function createChecker(program: Program): Checker { ); // TODO-TIM check if we expose this below - commit( - param, - param.constraint?.kind === "Value" ? unknownType : param.constraint ?? unknownType - ); + commit(param, param.constraint?.type ?? unknownType); } continue; } const [argNode, type] = init(); - + if (type === null) { + commit(param, unknownType); + continue; + } 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 (!checkTypeAssignable(type, constraint, argNode)) { - // TODO-TIM check if we expose this below - const effectiveType = param.constraint?.kind === "Value" ? unknownType : param.constraint; + 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; @@ -1109,6 +1561,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; } @@ -1132,6 +1595,27 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined, instantiateTemplates = true ): Type { + const result = checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplates); + if (result === null || isValue(result)) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); + return errorType; + } + if (result.entityKind === "Indeterminate") { + return result.type; + } + return result; + } + + function checkTypeOrValueReferenceSymbol( + sym: Sym, + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + mapper: TypeMapper | undefined, + instantiateTemplates = true + ): Type | Value | IndeterminateEntity | null { + 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 }) @@ -1150,7 +1634,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 | @@ -1214,13 +1698,14 @@ 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) { - 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; @@ -1247,6 +1732,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; } @@ -1291,23 +1785,26 @@ 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); + + return type; } function getOrInstantiateTemplate( templateNode: TemplateableNode, params: TemplateParameter[], - args: Type[], + args: (Type | Value | IndeterminateEntity)[], parentMapper: TypeMapper | undefined, instantiateTempalates = true ): Type { @@ -1371,6 +1868,67 @@ 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 checkMixedParameterConstraintUnion( + node: UnionExpressionNode, + mapper: TypeMapper | undefined + ): MixedParameterConstraint { + const values: Type[] = []; + const types: Type[] = []; + for (const option of node.options) { + const [kind, type] = getTypeOrValueOfTypeForNode(option, mapper); + if (kind === "value") { + values.push(type); + } else { + types.push(type); + } + } + return { + entityKind: "MixedParameterConstraint", + node, + valueType: + values.length === 0 + ? undefined + : 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", @@ -1411,17 +1969,6 @@ export function createChecker(program: Program): Checker { return unionType; } - function checkValueOfExpression( - node: ValueOfExpressionNode, - mapper: TypeMapper | undefined - ): ValueType { - 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. @@ -1466,8 +2013,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 ?? (() => {}), }); @@ -1475,9 +2022,50 @@ export function createChecker(program: Program): Checker { linkType(links, decoratorType, mapper); + checkDecoratorLegacyMarshalling(decoratorType); return decoratorType; } + 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.`, + 'Add `export const $flags = {decoratorArgMarshalling: "new"}}` to your library to opt-in to the new marshalling behavior.', + ].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))) { + 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) + ) { + reportDeprecatedLegacyMarshalling( + param, + "a numeric type that is not representable as a JS Number" + ); + } + } + } + } + } + function checkFunctionDeclaration( node: FunctionDeclarationStatementNode, mapper: TypeMapper | undefined @@ -1509,7 +2097,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 ?? (() => {}), }); @@ -1523,7 +2111,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 + ): SignatureFunctionParameter; + function checkFunctionParameter( + node: FunctionParameterNode, + mapper: TypeMapper | undefined, + mixed: boolean ): FunctionParameter { const links = getSymbolLinks(node.symbol); @@ -1543,27 +2142,70 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "rest-parameter-array", target: node.type }) ); } - const type = node.type ? getTypeOrValueTypeForNode(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) + : ({ + entityKind: "MixedParameterConstraint", + type: unknownType, + } satisfies MixedParameterConstraint); + 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; } - function getTypeOrValueTypeForNode(node: Node, mapper?: TypeMapper) { - if (node.kind === SyntaxKind.ValueOfExpression) { - return checkValueOfExpression(node, mapper); + function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): ["type" | "value", Type] { + switch (node.kind) { + case SyntaxKind.ValueOfExpression: + const target = getTypeForNode(node.target, mapper); + return ["value", target]; + default: + return ["type", getTypeForNode(node, mapper)]; + } + } + + function getParamConstraintEntityForNode( + node: Expression, + mapper?: TypeMapper + ): MixedParameterConstraint { + switch (node.kind) { + case SyntaxKind.UnionExpression: + return checkMixedParameterConstraintUnion(node, mapper); + default: + const [kind, entity] = getTypeOrValueOfTypeForNode(node, mapper); + return { + entityKind: "MixedParameterConstraint", + node: node, + type: kind === "value" ? undefined : entity, + valueType: kind === "value" ? entity : undefined, + }; } - return getTypeForNode(node, mapper); } function mergeModelTypes( @@ -1676,7 +2318,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); @@ -2062,7 +2704,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. } @@ -2634,19 +3279,125 @@ 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, - }); + ): IndeterminateEntity | StringValue | null { + let hasType = false; + let hasValue = false; + const spanTypeOrValues = node.spans.map( + (span) => [span, checkNode(span.expression, mapper)] as const + ); + for (const [_, typeOrValue] of spanTypeOrValues) { + 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; + } + } + } - return type; + if (hasType && hasValue) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "mixed-string-template", + target: node, + }) + ); + return null; + } + + if (hasValue) { + let str = node.head.value; + for (const [span, typeOrValue] of spanTypeOrValues) { + 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); + } else { + let hasNonStringElement = false; + let stringValue = node.head.value; + + const spans: StringTemplateSpan[] = [createTemplateSpanLiteral(node.head)]; + + for (const [span, typeOrValue] of spanTypeOrValues) { + compilerAssert(typeOrValue !== null && !isValue(typeOrValue), "Expected type."); + + const type = typeOrValue.entityKind === "Indeterminate" ? typeOrValue.type : typeOrValue; + const spanValue = createTemplateSpanValue(span.expression, type); + spans.push(spanValue); + const spanValueAsString = stringifyTypeForTemplate(type); + if (spanValueAsString) { + stringValue += spanValueAsString; + } else { + hasNonStringElement = true; + } + + spans.push(createTemplateSpanLiteral(span.literal)); + stringValue += span.literal.value; + } + return createIndeterminateEntity( + createType({ + kind: "StringTemplate", + node, + spans, + stringValue: hasNonStringElement ? undefined : stringValue, + }) + ); + } + } + + function createIndeterminateEntity(type: IndeterminateEntity["type"]): IndeterminateEntity { + return { + entityKind: "Indeterminate", + type, + }; + } + 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": + case "NumericValue": + case "BooleanValue": + return value.value.toString(); + default: + reportCheckerDiagnostic( + createDiagnostic({ + code: "non-literal-string-template", + target: value, + }) + ); + return `[${value.valueKind}]`; + } } function createTemplateSpanLiteral( @@ -2660,28 +3411,34 @@ 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, }); } - function checkStringLiteral(str: StringLiteralNode): StringLiteral { - return getLiteralType(str); + function checkStringLiteral(str: StringLiteralNode): IndeterminateEntity { + return { + entityKind: "Indeterminate", + type: getLiteralType(str), + }; } - function checkNumericLiteral(num: NumericLiteralNode): NumericLiteral { - return getLiteralType(num); + function checkNumericLiteral(num: NumericLiteralNode): IndeterminateEntity { + return { + entityKind: "Indeterminate", + type: getLiteralType(num), + }; } - function checkBooleanLiteral(bool: BooleanLiteralNode): BooleanLiteral { - return getLiteralType(bool); + function checkBooleanLiteral(bool: BooleanLiteralNode): IndeterminateEntity { + return { + entityKind: "Indeterminate", + type: getLiteralType(bool), + }; } function checkProgram() { @@ -2737,7 +3494,7 @@ export function createChecker(program: Program): Checker { function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { - getTypeForNode(statement, undefined); + checkNode(statement, undefined); } } @@ -2865,7 +3622,12 @@ 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) => t.kind !== "TemplateParameter"); + return ( + !mapper.partial && + mapper.args.every( + (t) => isValue(t) || t.entityKind === "Indeterminate" || t.kind !== "TemplateParameter" + ) + ); } function checkModelExpression(node: ModelExpressionNode, mapper: TypeMapper | undefined) { @@ -2988,6 +3750,510 @@ export function createChecker(program: Program): Checker { } } + function checkObjectValue( + node: ObjectLiteralNode, + mapper: TypeMapper | undefined, + 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; + } + return { + entityKind: "Value", + valueKind: "ObjectValue", + node: node, + properties, + type: constraint ? constraint.type : preciseType, + }; + } + + function createTypeForObjectValue( + node: ObjectLiteralNode, + properties: Map + ): Model { + const model = createType({ + kind: "Model", + name: "", + node, + properties: createRekeyableMap(), + decorators: [], + derivedModels: [], + sourceModels: [], + }); + + for (const prop of properties.values()) { + model.properties.set(prop.name, createModelPropertyForObjectPropertyDescriptor(prop, model)); + } + return finishType(model); + } + + function createModelPropertyForObjectPropertyDescriptor( + prop: ObjectValuePropertyDescriptor, + parentModel: Model + ): ModelProperty { + return createAndFinishType({ + kind: "ModelProperty", + node: prop.node, + model: parentModel, + optional: false, + name: prop.name, + type: prop.value.type, + decorators: [], + }); + } + + function checkObjectLiteralProperties( + node: ObjectLiteralNode, + mapper: TypeMapper | undefined + ): 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) { + hasError = true; + } else { + 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 }); + } + } + } + } + return hasError ? null : properties; + } + + function checkObjectSpreadProperty( + targetNode: TypeReferenceNode, + mapper: TypeMapper | undefined + ): ObjectValue | null { + const value = getValueForNode(targetNode, mapper); + if (value === null) { + return null; + } + if (value.valueKind !== "ObjectValue") { + reportCheckerDiagnostic(createDiagnostic({ code: "spread-object", target: targetNode })); + return null; + } + + return value; + } + + function checkArrayValue( + node: ArrayLiteralNode, + mapper: TypeMapper | undefined, + constraint: CheckValueConstraint | 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; + } + + const preciseType = createTypeForArrayValue(node, values as any); + if (constraint && !checkTypeOfValueMatchConstraint(preciseType, constraint, node)) { + return null; + } + + return { + entityKind: "Value", + valueKind: "ArrayValue", + node: node, + values: values as any, + type: constraint ? constraint.type : preciseType, + }; + } + + function createTypeForArrayValue(node: ArrayLiteralNode, values: Value[]): Tuple { + return createAndFinishType({ + kind: "Tuple", + node, + values: values.map((x) => x.type), + }); + } + + function inferScalarForPrimitiveValue( + type: Type | undefined, + literalType: Type + ): Scalar | undefined { + if (type === undefined) { + return undefined; + } + switch (type.kind) { + case "Scalar": + 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(variant.type, literalType); + if (scalar) { + if (found) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "ambiguous-scalar-type", + format: { + value: getTypeName(literalType), + types: [found, scalar].map((x) => x.name).join(", "), + example: found.name, + }, + target: literalType, + }) + ); + return undefined; + } else { + found = scalar; + } + } + } + return found; + default: + return undefined; + } + } + + function checkStringValue( + literalType: StringLiteral | StringTemplate, + constraint: CheckValueConstraint | undefined, + node: Node + ): StringValue | null { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { + return null; + } + let value: string; + if (literalType.kind === "StringTemplate") { + if (literalType.stringValue) { + value = literalType.stringValue; + } else { + reportCheckerDiagnostics(explainStringTemplateNotSerializable(literalType)); + return null; + } + } else { + value = literalType.value; + } + const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); + return { + entityKind: "Value", + valueKind: "StringValue", + value, + type: constraint ? constraint.type : literalType, + scalar, + }; + } + + function checkNumericValue( + literalType: NumericLiteral, + constraint: CheckValueConstraint | undefined, + node: Node + ): NumericValue | null { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { + return null; + } + const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); + return { + entityKind: "Value", + valueKind: "NumericValue", + value: Numeric(literalType.valueAsString), + type: constraint ? constraint.type : literalType, + scalar, + }; + } + + function checkBooleanValue( + literalType: BooleanLiteral, + constraint: CheckValueConstraint | undefined, + node: Node + ): BooleanValue | null { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { + return null; + } + const scalar = inferScalarForPrimitiveValue(constraint?.type, literalType); + return { + entityKind: "Value", + valueKind: "BooleanValue", + value: literalType.value, + type: constraint ? constraint.type : literalType, + scalar, + }; + } + + function checkNullValue( + literalType: NullType, + constraint: CheckValueConstraint | undefined, + node: Node + ): NullValue | null { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { + return null; + } + + return { + entityKind: "Value", + + valueKind: "NullValue", + type: constraint ? constraint.type : literalType, + value: null, + }; + } + + function checkEnumValue( + literalType: EnumMember, + constraint: CheckValueConstraint | undefined, + node: Node + ): EnumValue | null { + if (constraint && !checkTypeOfValueMatchConstraint(literalType, constraint, node)) { + return null; + } + return { + entityKind: "Value", + + valueKind: "EnumValue", + type: constraint ? constraint.type : literalType, + value: literalType, + }; + } + + function checkCallExpressionTarget( + node: CallExpressionNode, + mapper: TypeMapper | undefined + ): ScalarConstructor | Scalar | null { + const target = checkTypeReference(node.target, mapper); + if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { + return target; + } else { + reportCheckerDiagnostic( + createDiagnostic({ + code: "non-callable", + format: { type: target.kind }, + target: node.target, + }) + ); + return null; + } + } + + /** Check the arguments of the call expression are a single value of the given syntax. */ + function checkPrimitiveArg( + node: CallExpressionNode, + scalar: Scalar, + valueKind: T["valueKind"] + ): T | null { + if (node.arguments.length !== 1) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-primitive-init", + target: node.target, + }) + ); + return null; + } + const argNode = node.arguments[0]; + const value = getValueForNode(argNode, undefined); + if (value === null) { + return null; // error should already have been reported above. + } + if (value.valueKind !== valueKind) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-primitive-init", + messageId: "invalidArg", + format: { actual: value.valueKind, expected: valueKind }, + target: argNode, + }) + ); + return null; + } + if (!checkValueOfType(value, scalar, argNode)) { + return null; + } + 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 = 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, { kind: "argument", type: 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, { + kind: "argument", + type: parameter.type, + }); + if (arg === null) { + hasError = true; + continue; + } + if (checkValueOfType(arg, parameter.type, argNode)) { + resolvedArgs.push(arg); + } else { + hasError = true; + } + } + } + if (hasError) { + return null; + } + return { + entityKind: "Value", + valueKind: "ScalarValue", + value: { + name: declaration.name, + args: resolvedArgs, + }, + scalar: declaration.scalar, + type: declaration.scalar, + }; + } + + function checkCallExpression( + node: CallExpressionNode, + mapper: TypeMapper | undefined + ): Value | null { + const target = checkCallExpressionTarget(node, mapper); + if (target === null) { + return null; + } + if (target.kind === "ScalarConstructor") { + return createScalarValue(node, mapper, target); + } + + if (areScalarsRelated(target, getStdType("string"))) { + return checkPrimitiveArg(node, target, "StringValue"); + } else if (areScalarsRelated(target, getStdType("numeric"))) { + return checkPrimitiveArg(node, target, "NumericValue"); + } else if (areScalarsRelated(target, getStdType("boolean"))) { + return checkPrimitiveArg(node, target, "BooleanValue"); + } else { + reportCheckerDiagnostic( + createDiagnostic({ + code: "named-init-required", + format: { typeKind: target.kind }, + target: node.target, + }) + ); + return null; + } + } + + function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { + 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; + } + if (entity.entityKind === "Indeterminate") { + return entity.type; + } + + if (isType(entity)) { + 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; + } + function createUnion(options: Type[]): Union { const variants = createRekeyableMap(); const union: Union = createAndFinishType({ @@ -3086,6 +4352,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) { @@ -3292,6 +4567,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); @@ -3574,7 +4854,14 @@ 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); + if (prop.default) { + const defaultValue = checkDefaultValue(prop.default, type.type); + if (defaultValue !== null) { + type.defaultValue = defaultValue; + // eslint-disable-next-line deprecation/deprecation + type.default = checkLegacyDefault(prop.default); + } + } if (links) { linkType(links, type, mapper); } @@ -3612,46 +4899,48 @@ 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; - } - if (type.kind === "UnionVariant") { - return isValueType(type.type); - } - const valueTypes = new Set(["String", "Number", "Boolean", "EnumMember", "Tuple"]); - return valueTypes.has(type.kind); - } - - function checkDefault(defaultNode: Node, type: Type): Type { - const defaultType = getTypeForNode(defaultNode, undefined); + function checkDefaultValue(defaultNode: Node, type: Type): Value | null { if (isErrorType(type)) { - return errorType; - } - if (!isValueType(defaultType)) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "unsupported-default", - format: { type: defaultType.kind }, - target: defaultNode, - }) - ); - return errorType; + // if the prop type is an error we don't need to validate again. + return null; + } + const defaultValue = getValueForNode( + defaultNode, + undefined, + { + kind: "assignment", + type, + }, + { legacyTupleAndModelCast: true } + ); + if (defaultValue === null) { + return null; } - const [related, diagnostics] = isTypeAssignableTo(defaultType, type, defaultNode); + const [related, diagnostics] = isValueOfType(defaultValue, type, defaultNode); if (!related) { reportCheckerDiagnostics(diagnostics); - return errorType; + return null; } else { - return defaultType; + return { ...defaultValue, type }; + } + } + + /** + * Fill in the legacy `.default` property. + * We do do checking here we just keep existing behavior. + */ + function checkLegacyDefault(defaultNode: Node): Type | undefined { + const resolved = checkNode(defaultNode, undefined); + if (resolved === null || isValue(resolved)) { + return undefined; + } + if (resolved.entityKind === "Indeterminate") { + return resolved.type; } + return resolved; } - function checkDecorator( + function checkDecoratorApplication( targetType: Type, decNode: DecoratorExpressionNode | AugmentDecoratorStatementNode, mapper: TypeMapper | undefined @@ -3679,7 +4968,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 = @@ -3693,15 +4981,23 @@ 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 { definition: symbolLinks.declaredType, decorator: sym.value ?? ((...args: any[]) => {}), @@ -3710,16 +5006,27 @@ export function createChecker(program: Program): Checker { }; } - function checkDecoratorUsage( - targetType: Type, - declaration: Decorator, - args: DecoratorArgument[], - decoratorNode: Node - ): [boolean, DecoratorArgument[]] { - let hasError = false; + function resolveDecoratorArgMarshalling(declaredType: Decorator | undefined): "new" | "legacy" { + if (declaredType) { + const location = getLocationContext(program, declaredType); + if (location.type === "compiler") { + return "new"; + } else if ( + (location.type === "library" || location.type === "project") && + location.flags?.decoratorArgMarshalling + ) { + return location.flags.decoratorArgMarshalling; + } else { + return "legacy"; + } + } + return "new"; + } + /** 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", @@ -3727,21 +5034,52 @@ 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, }) ); } - 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 => { + let type = checkNode(argNode, mapper) ?? errorType; + if (type.entityKind === "Indeterminate") { + type = type.type; + } + 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; } @@ -3750,8 +5088,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 { @@ -3759,28 +5097,57 @@ 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[] = []; + const jsMarshalling = resolveDecoratorArgMarshalling(declaration); + function resolveArg( + argNode: Expression, + perParamType: MixedParameterConstraint + ): 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 = getIndexType( - parameter.type.kind === "Value" ? parameter.type.target : parameter.type - ); + const restType = extractRestParamConstraint(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"), - }); - if (!checkArgumentAssignable(arg.value, restType, arg.node!)) { + for (let i = index; i < node.arguments.length; i++) { + const argNode = node.arguments[i]; + if (argNode) { + const arg = resolveArg(argNode, restType); + if (arg) { + resolvedArgs.push(arg); + } else { hasError = true; } } @@ -3788,13 +5155,12 @@ export function createChecker(program: Program): Checker { } break; } - 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!)) { + const argNode = node.arguments[index]; + if (argNode) { + const arg = resolveArg(argNode, parameter.type); + if (arg) { + resolvedArgs.push(arg); + } else { hasError = true; } } @@ -3802,24 +5168,61 @@ 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: MixedParameterConstraint + ): MixedParameterConstraint | undefined { + let valueType: Type | undefined; + let type: Type | undefined; + if (constraint.valueType) { + if ( + constraint.valueType.kind === "Model" && + isArrayModelType(program, constraint.valueType) + ) { + valueType = constraint.valueType.indexer.value; + } else { + return undefined; + } + } + if (constraint.type) { + if (constraint.type.kind === "Model" && isArrayModelType(program, constraint.type)) { + type = constraint.type.indexer.value; + } else { + return undefined; + } + } + + return { + entityKind: "MixedParameterConstraint", + type, + valueType, + }; + } + function getIndexType(type: Type): Type | undefined { return type.kind === "Model" ? type.indexer?.value : undefined; } - 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]; + function resolveDecoratorArgJsValue( + value: Type | Value, + valueConstraint: CheckValueConstraint | undefined, + jsMarshalling: "legacy" | "new" + ) { + if (valueConstraint !== undefined) { + if (isValue(value)) { + return jsMarshalling === "legacy" + ? legacyMarshallTypeForJS(checker, value) + : marshallTypeForJS(value, valueConstraint.type); + } else { + return value; } } return value; } function checkArgumentAssignable( - argumentType: Type, - parameterType: Type | ValueType, + argumentType: Type | Value | IndeterminateEntity, + parameterType: Entity, diagnosticTarget: DiagnosticTarget ): boolean { const [valid] = isTypeAssignableTo(argumentType, parameterType, diagnosticTarget); @@ -3828,8 +5231,8 @@ export function createChecker(program: Program): Checker { createDiagnostic({ code: "invalid-argument", format: { - value: getTypeName(argumentType), - expected: getTypeName(parameterType), + value: getEntityName(argumentType), + expected: getEntityName(parameterType), }, target: diagnosticTarget, }) @@ -3843,7 +5246,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); } @@ -3864,7 +5267,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); } @@ -3887,20 +5290,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 = getTypeForNode(argNode, mapper); - return { - value: type, - jsValue: type, - node: argNode, - }; - }); - } - function checkScalar(node: ScalarStatementNode, mapper: TypeMapper | undefined): Scalar { const links = getSymbolLinks(node.symbol); @@ -3916,10 +5305,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) { @@ -3987,6 +5378,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 scalar constructor 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, false)), + }); + 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); @@ -4010,12 +5453,64 @@ export function createChecker(program: Program): Checker { return errorType; } - pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = getTypeForNode(node.value, mapper); - linkType(links, type, mapper); - pendingResolutions.finish(aliasSymId, ResolutionKind.Type); + pendingResolutions.start(aliasSymId, ResolutionKind.Type); + const type = getTypeForNode(node.value, mapper); + if (!isValue(type)) { + linkType(links, type, mapper); + } + pendingResolutions.finish(aliasSymId, ResolutionKind.Type); + + return type; + } + + function checkConst(node: ConstStatementNode): Value | null { + const links = getSymbolLinks(node.symbol); + if (links.value !== undefined) { + return links.value; + } + + 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 null; + } + + pendingResolutions.start(symId, ResolutionKind.Value); + 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; + return links.value; + } + links.value = type ? { ...value, type } : { ...value }; + return links.value; + } - return type; + 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 { @@ -4414,7 +5909,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; } @@ -4426,17 +5921,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; } @@ -5259,7 +6754,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: "", @@ -5314,6 +6809,7 @@ export function createChecker(program: Program): Checker { kind: "Number", value, valueAsString, + numericValue: Numeric(valueAsString), }); break; } @@ -5328,7 +6824,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(unsafe_projectionArgumentMarshalForJS)); return voidType; }); } @@ -5355,7 +6851,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(unsafe_projectionArgumentMarshalForJS)); return marshalProjectionReturn(retval, { functionName: node.sv }); }); return t; @@ -5458,6 +6954,37 @@ 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 { + 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 @@ -5465,8 +6992,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: Entity | IndeterminateEntity, + target: Entity, diagnosticTarget: DiagnosticTarget ): boolean { const [related, diagnostics] = isTypeAssignableTo(source, target, diagnosticTarget); @@ -5476,6 +7003,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 @@ -5483,24 +7022,44 @@ 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: Entity | IndeterminateEntity, + target: Entity, 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]; + } + + /** + * 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 | ValueType, - target: Type | ValueType, + source: Entity | IndeterminateEntity, + target: Entity, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity | IndeterminateEntity, Entity], Related> ): [Related, readonly Diagnostic[]] { const cached = relationCache.get([source, target]); if (cached !== undefined) { @@ -5510,31 +7069,39 @@ 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 | IndeterminateEntity, + 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") { + // BACKCOMPAT: Allow certain type to be accepted as values + if ( + "kind" in source && + "entityKind" in target && + source.kind === "TemplateParameter" && + source.constraint?.type && + source.constraint.valueType === undefined && + target.entityKind === "MixedParameterConstraint" && + target.valueType + ) { const [assignable] = isTypeAssignableToInternal( - source.constraint, - target.target, + source.constraint.type, + target.valueType, diagnosticTarget, 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 @@ -5543,18 +7110,38 @@ export function createChecker(program: Program): Checker { } } - while (source.kind === "TemplateParameter" && source.constraint !== source) { + if ("kind" in source && source.kind === "TemplateParameter") { source = source.constraint ?? unknownType; } + if (target.entityKind === "Indeterminate") { + target = target.type; + } if (source === target) return [Related.true, []]; - if (target.kind === "Value") { - return isAssignableToValueType(source, target, diagnosticTarget, relationCache); + + if (isValue(target)) { + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; + } + if (source.entityKind === "Indeterminate") { + return isIndeterminateEntityAssignableTo(source, target, diagnosticTarget, relationCache); + } + + if (target.entityKind === "MixedParameterConstraint") { + return isAssignableToMixedParameterConstraint( + source, + target, + diagnosticTarget, + relationCache + ); } - if (source.kind === "Value") { + if (isValue(source) || (source.entityKind === "MixedParameterConstraint" && source.valueType)) { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } + if (source.entityKind === "MixedParameterConstraint") { + return isTypeAssignableToInternal(source.type!, target, diagnosticTarget, relationCache); + } + const isSimpleTypeRelated = isSimpleTypeAssignableTo(source, target); if (isSimpleTypeRelated === true) { return [Related.true, []]; @@ -5603,27 +7190,25 @@ export function createChecker(program: Program): Checker { isArrayModelType(program, target) && source.kind === "Model" ) { - return hasIndexAndIsAssignableTo( - source, - target as Model & { indexer: ModelIndexer }, - diagnosticTarget, - relationCache - ); - } else if (target.kind === "Model" && source.kind === "Model") { - return isModelRelatedTo(source, target, diagnosticTarget, relationCache); - } else if (target.kind === "Model" && target.indexer && source.kind === "Tuple") { - for (const item of source.values) { - const [related, diagnostics] = isTypeAssignableToInternal( - item, - target.indexer.value!, + if (isArrayModelType(program, source)) { + return hasIndexAndIsAssignableTo( + source, + target as Model & { indexer: ModelIndexer }, diagnosticTarget, relationCache ); - if (!related) { - return [Related.false, diagnostics]; - } + } else { + // For other models just fallback to unassignable + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - return [Related.true, []]; + } else if (target.kind === "Model" && source.kind === "Model") { + return isModelRelatedTo(source, target, diagnosticTarget, relationCache); + } else if ( + target.kind === "Model" && + isArrayModelType(program, target) && + source.kind === "Tuple" + ) { + 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") { @@ -5635,34 +7220,118 @@ export function createChecker(program: Program): Checker { return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - function isAssignableToValueType( - source: Type | ValueType, - target: ValueType, + function isIndeterminateEntityAssignableTo( + indeterminate: IndeterminateEntity, + target: Type | MixedParameterConstraint, diagnosticTarget: DiagnosticTarget, - relationCache: MultiKeyMap<[Type | ValueType, Type | ValueType], Related> + relationCache: MultiKeyMap<[Entity, Entity], Related> ): [Related, readonly Diagnostic[]] { - if (source.kind === "Value") { - return isTypeAssignableToInternal( - source.target, - target.target, + const [typeRelated, typeDiagnostics] = isTypeAssignableToInternal( + indeterminate.type, + target, + diagnosticTarget, + relationCache + ); + if (typeRelated) { + return [Related.true, []]; + } + + if (target.entityKind === "MixedParameterConstraint" && target.valueType) { + const [valueRelated] = isTypeAssignableToInternal( + indeterminate.type, + target.valueType, diagnosticTarget, relationCache ); + + if (valueRelated) { + return [Related.true, []]; + } } - const [assignable, diagnostics] = isTypeAssignableToInternal( - source, - target.target, - diagnosticTarget, - relationCache - ); - if (!assignable) { - return [assignable, diagnostics]; + + return [Related.false, typeDiagnostics]; + } + + function isAssignableToValueType( + source: Entity, + target: Type, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Entity, Entity], Related> + ): [Related, readonly Diagnostic[]] { + if (!isValue(source)) { + return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - if (!isValueType(source)) { + return isValueOfTypeInternal(source, target, diagnosticTarget, relationCache); + } + + function isAssignableToMixedParameterConstraint( + source: Entity, + target: MixedParameterConstraint, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Entity, Entity], Related> + ): [Related, readonly Diagnostic[]] { + if ("entityKind" in source && source.entityKind === "MixedParameterConstraint") { + if (source.type && target.type) { + const [variantAssignable, diagnostics] = isTypeAssignableToInternal( + source.type, + target.type, + diagnosticTarget, + relationCache + ); + if (variantAssignable === Related.false) { + return [Related.false, diagnostics]; + } + return [Related.true, []]; + } + if (source.valueType && target.valueType) { + const [variantAssignable, diagnostics] = isTypeAssignableToInternal( + source.valueType, + target.valueType, + diagnosticTarget, + relationCache + ); + if (variantAssignable === Related.false) { + return [Related.false, diagnostics]; + } + return [Related.true, []]; + } return [Related.false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; } - return [Related.true, []]; + + if (target.type) { + const [related] = isTypeAssignableToInternal( + source, + target.type, + diagnosticTarget, + relationCache + ); + if (related) { + return [Related.true, []]; + } + } + if (target.valueType) { + const [related] = isAssignableToValueType( + source, + target.valueType, + 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 isValueOfTypeInternal( + source: Value, + target: Type, + diagnosticTarget: DiagnosticTarget, + relationCache: MultiKeyMap<[Entity, Entity], Related> + ): [Related, readonly Diagnostic[]] { + return isTypeAssignableToInternal(source.type, target, diagnosticTarget, relationCache); } function isReflectionType(type: Type): type is Model & { name: ReflectionTypeName } { @@ -5679,7 +7348,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": @@ -5719,7 +7388,16 @@ 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 === "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; @@ -5728,6 +7406,31 @@ export function createChecker(program: Program): Checker { } function isNumericLiteralRelatedTo(source: NumericLiteral, target: Scalar) { + // 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) { // if the target does not derive from numeric, then it can't be assigned a numeric literal if (!areScalarsRelated(target, getStdType("numeric"))) { return false; @@ -5747,20 +7450,40 @@ 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); + const [low, high, options] = numericRanges[target.name as keyof typeof numericRanges]; + 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, 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[] = []; @@ -5830,11 +7553,37 @@ export function createChecker(program: Program): Checker { diagnostics.push(...indexDiagnostics); } } + } else if (shouldCheckExcessProperties(source)) { + for (const [propName, prop] of remainingProperties) { + if (shouldCheckExcessProperty(prop)) { + diagnostics.push( + createDiagnostic({ + code: "unexpected-property", + format: { + propertyName: propName, + type: getEntityName(target), + }, + target: prop, + }) + ); + } + } } return [diagnostics.length === 0 ? Related.true : Related.false, diagnostics]; } + /** 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 ( + prop.node?.kind === SyntaxKind.ObjectLiteralProperty && prop.node.parent === prop.model?.node + ); + } + function getProperty(model: Model, name: string): ModelProperty | undefined { return ( model.properties.get(name) ?? @@ -5868,7 +7617,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[]] { if (source.indexer === undefined || source.indexer.key !== target.indexer.key) { return [ @@ -5893,11 +7642,67 @@ export function createChecker(program: Program): Checker { ); } - function isTupleAssignableToTuple( + 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, 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 [ @@ -5907,7 +7712,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}.`, }, @@ -5935,7 +7740,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, []]; @@ -5978,13 +7783,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, }); } @@ -6013,27 +7818,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 }] -> = { - 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 }], -}; - /** * 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 @@ -6073,10 +7857,12 @@ function addDerivedModels(models: Set, possiblyDerivedModels: ReadonlySet function createTypeMapper( parameters: TemplateParameter[], - args: Type[], + 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]); @@ -6406,7 +8192,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; } @@ -6471,26 +8257,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; } @@ -6516,6 +8282,7 @@ const ReflectionNameToKind = { const _assertReflectionNameToKind: Record = ReflectionNameToKind; enum ResolutionKind { + Value, Type, BaseType, Constraint, @@ -6573,3 +8340,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 arg.stringValue; + } + return arg as any; +} diff --git a/packages/compiler/src/core/compiler-code-fixes/model-to-object-literal.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/model-to-object-literal.codefix.ts new file mode 100644 index 0000000000..3b96de6154 --- /dev/null +++ b/packages/compiler/src/core/compiler-code-fixes/model-to-object-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 value. + */ +export function createModelToObjectValueCodeFix(node: ModelExpressionNode) { + return defineCodeFix({ + id: "model-to-object-value", + label: `Convert to an object value \`#{}\``, + fix: (context) => { + const location = getSourceLocation(node); + return context.prependText(location, "#"); + }, + }); +} diff --git a/packages/compiler/src/core/compiler-code-fixes/tuple-to-array-value.codefix.ts b/packages/compiler/src/core/compiler-code-fixes/tuple-to-array-value.codefix.ts new file mode 100644 index 0000000000..4a7855426d --- /dev/null +++ b/packages/compiler/src/core/compiler-code-fixes/tuple-to-array-value.codefix.ts @@ -0,0 +1,16 @@ +import { defineCodeFix, getSourceLocation } from "../diagnostics.js"; +import type { TupleExpressionNode } from "../types.js"; + +/** + * Quick fix that convert a tuple to an array value. + */ +export function createTupleToArrayValueCodeFix(node: TupleExpressionNode) { + return defineCodeFix({ + id: "tuple-to-array-value", + label: `Convert to an array value \`#[]\``, + fix: (context) => { + const location = getSourceLocation(node); + return context.prependText(location, "#"); + }, + }); +} 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 { - 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)); - 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) { @@ -56,7 +43,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.valueType !== undefined) { break; // Value types will be serializable in the template instance. } // eslint-disable-next-line no-fallthrough @@ -70,5 +57,5 @@ export function isStringTemplateSerializable( } } } - return [diagnostics.diagnostics.length === 0, diagnostics.diagnostics]; + return diagnostics.diagnostics; } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 5dfdd9cffa..88088dcb07 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -1,6 +1,8 @@ import { printId } from "../../formatter/print/printer.js"; -import { isTemplateInstance } from "../type-utils.js"; -import { +import { isDefined } from "../../utils/misc.js"; +import { isTemplateInstance, isType, isValue } from "../type-utils.js"; +import type { + Entity, Enum, Interface, Model, @@ -9,9 +11,10 @@ import { Namespace, Operation, Scalar, + StringTemplate, Type, Union, - ValueType, + Value, } from "../types.js"; export interface TypeNameOptions { @@ -19,7 +22,7 @@ export interface TypeNameOptions { printable?: boolean; } -export function getTypeName(type: Type | ValueType, options?: TypeNameOptions): string { +export function getTypeName(type: Type, options?: TypeNameOptions): string { switch (type.kind) { case "Namespace": return getNamespaceFullName(type, options); @@ -46,19 +49,59 @@ export function getTypeName(type: Type | ValueType, options?: TypeNameOptions): 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": + return type.valueAsString; case "Boolean": return type.value.toString(); case "Intrinsic": return type.name; - case "Value": - return `valueof ${getTypeName(type.target, options)}`; + 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 "EnumValue": + 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 if (isType(entity)) { + return getTypeName(entity, options); + } else { + switch (entity.entityKind) { + case "MixedParameterConstraint": + return [ + entity.type && getEntityName(entity.type), + entity.valueType && `valueof ${getEntityName(entity.valueType)}`, + ] + .filter(isDefined) + .join(" | "); + case "Indeterminate": + return getTypeName(entity.type, options); + } + } } export function isStdNamespace(namespace: Namespace): boolean { @@ -122,12 +165,15 @@ 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)) { // 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 @@ -175,7 +221,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}`; @@ -197,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/src/core/index.ts b/packages/compiler/src/core/index.ts index 2b4eebfda6..c410fd9c64 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -6,12 +6,32 @@ 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-state.js"; export { // eslint-disable-next-line deprecation/deprecation createCadlLibrary, createLinterRule as createRule, createTypeSpecLibrary, defineLinter, + definePackageFlags, paramMessage, // eslint-disable-next-line deprecation/deprecation setCadlNamespace, @@ -19,6 +39,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/intrinsic-type-state.ts b/packages/compiler/src/core/intrinsic-type-state.ts new file mode 100644 index 0000000000..9d63a9b47a --- /dev/null +++ b/packages/compiler/src/core/intrinsic-type-state.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.maxLength).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.maxLength).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/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts new file mode 100644 index 0000000000..2d16f1bbd1 --- /dev/null +++ b/packages/compiler/src/core/js-marshaller.ts @@ -0,0 +1,101 @@ +import { Checker } from "./checker.js"; +import { compilerAssert } from "./diagnostics.js"; +import { numericRanges } from "./numeric-ranges.js"; +import { Numeric } from "./numeric.js"; +import type { + ArrayValue, + MarshalledValue, + NumericValue, + ObjectValue, + Type, + Value, +} from "./types.js"; + +/** + * 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( + value: T, + valueConstraint: Type | undefined +): MarshalledValue { + switch (value.valueKind) { + case "BooleanValue": + case "StringValue": + return value.value as any; + case "NumericValue": + return numericValueToJs(value, valueConstraint) as any; + case "ObjectValue": + return objectValueToJs(value) as any; + case "ArrayValue": + return arrayValueToJs(value) as any; + case "EnumValue": + return value as any; + case "NullValue": + return null as any; + case "ScalarValue": + return value as any; + } +} + +export function canNumericConstraintBeJsNumber(type: Type | undefined): boolean { + if (type === undefined) return true; + switch (type.kind) { + case "Scalar": + return numericRanges[type.name as keyof typeof numericRanges]?.[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, undefined); + } + return result; +} +function arrayValueToJs(type: ArrayValue) { + return type.values.map((x) => marshallTypeForJS(x, undefined)); +} diff --git a/packages/compiler/src/core/library.ts b/packages/compiler/src/core/library.ts index 4a8842b8fb..27330ad23c 100644 --- a/packages/compiler/src/core/library.ts +++ b/packages/compiler/src/core/library.ts @@ -7,6 +7,7 @@ import { JSONSchemaValidator, LinterDefinition, LinterRuleDefinition, + PackageFlags, StateDef, TypeSpecLibrary, TypeSpecLibraryDef, @@ -102,6 +103,10 @@ export function createTypeSpecLibrary< } } +export function definePackageFlags(flags: PackageFlags): PackageFlags { + return flags; +} + export function defineLinter(def: LinterDefinition): LinterDefinition { return def; } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 51fa9ffcda..442a6f9833 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 expects a value literal or value reference.", }, }, "trailing-token": { @@ -391,12 +392,53 @@ 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.", + }, + }, + "expect-value": { + 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.`, + }, + }, + "non-callable": { + severity: "error", + messages: { + default: paramMessage`Type ${"type"} 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"}.`, + }, + }, + "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: { @@ -410,6 +452,15 @@ const diagnostics = { default: paramMessage`Property '${"propName"}' is required in type '${"targetType"}' but here is optional.`, }, }, + "value-in-type": { + severity: "error", + messages: { + default: "A value cannot be 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": { severity: "error", messages: { @@ -428,6 +479,12 @@ const diagnostics = { default: paramMessage`Property '${"propertyName"}' is missing on type '${"sourceType"}' but required in '${"targetType"}'`, }, }, + "unexpected-property": { + severity: "error", + messages: { + default: paramMessage`Object value may only specify known properties, and '${"propertyName"}' does not exist in type '${"type"}'.`, + }, + }, "extends-interface": { severity: "error", messages: { @@ -458,6 +515,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: { @@ -613,6 +676,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: { @@ -710,7 +780,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": { @@ -884,6 +954,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/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/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 6bd6ab4205..0f18aa39be 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -16,10 +16,13 @@ import { import { AliasStatementNode, AnyKeywordNode, + ArrayLiteralNode, AugmentDecoratorStatementNode, BlockComment, BooleanLiteralNode, + CallExpressionNode, Comment, + ConstStatementNode, DeclarationNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, @@ -62,6 +65,9 @@ import { Node, NodeFlags, NumericLiteralNode, + ObjectLiteralNode, + ObjectLiteralPropertyNode, + ObjectLiteralSpreadPropertyNode, OperationSignature, OperationStatementNode, ParseOptions, @@ -88,6 +94,7 @@ import { ProjectionTupleExpressionNode, ProjectionUnionSelectorNode, ProjectionUnionVariantSelectorNode, + ScalarConstructorNode, ScalarStatementNode, SourceFile, Statement, @@ -103,6 +110,7 @@ import { TemplateParameterDeclarationNode, TextRange, TupleExpressionNode, + TypeOfExpressionNode, TypeReferenceNode, TypeSpecScriptNode, UnionStatementNode, @@ -125,7 +133,13 @@ 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 + | Token.HashBracket; type CloseToken = Token.CloseBrace | Token.CloseParen | Token.CloseBracket | Token.GreaterThan; type DelimiterToken = Token.Comma | Token.Semicolon; @@ -179,6 +193,11 @@ namespace ListKind { invalidAnnotationTarget: "expression", } as const; + export const FunctionArguments = { + ...OperationParameters, + invalidAnnotationTarget: "expression", + } as const; + export const ModelProperties = { ...PropertiesBase, open: Token.OpenBrace, @@ -187,6 +206,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, @@ -197,6 +224,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, @@ -253,6 +290,13 @@ namespace ListKind { close: Token.CloseBracket, } as const; + export const ArrayLiteral = { + ...ExpresionsBase, + allowEmpty: true, + open: Token.HashBracket, + close: Token.CloseBracket, + } as const; + export const FunctionParameters = { ...ExpresionsBase, allowEmpty: true, @@ -438,6 +482,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); @@ -538,6 +586,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); @@ -890,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 = parseMixedParameterConstraint(); } let def: Expression | undefined; if (parseOptional(Token.Equals)) { @@ -907,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 = parseMixedParameterConstraint(); + parseExpected(Token.CloseParen); + return expr; + } + + return parseIntersectionExpressionOrHigher(); + } + + function parseMixedParameterConstraint(): 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) @@ -957,6 +1043,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(); + + return { + kind: SyntaxKind.ObjectLiteralProperty, + id, + value, + ...finishNode(pos), + }; + } + function parseScalarStatement( pos: number, decorators: DecoratorExpressionNode[] @@ -966,12 +1092,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), }; @@ -984,6 +1112,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, "scalar member"); + + parseExpected(Token.InitKeyword); + const id = parseIdentifier(); + const parameters = parseFunctionParameters(); + return { + kind: SyntaxKind.ScalarConstructor, + id, + parameters, + ...finishNode(pos), + }; + } + function parseEnumStatement( pos: number, decorators: DecoratorExpressionNode[] @@ -1070,6 +1224,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(); } @@ -1155,11 +1332,74 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseTypeOfExpression(): TypeOfExpressionNode { + const pos = tokenPos(); + parseExpected(Token.TypeOfKeyword); + const target = parseTypeOfTarget(); + + return { + kind: SyntaxKind.TypeOfExpression, + target, + ...finishNode(pos), + }; + } + + 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 { 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 (token() === 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 { @@ -1391,10 +1631,10 @@ 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: - return parseReferenceExpression(); + return parseCallOrReferenceExpression(); case Token.StringLiteral: return parseStringLiteral(); case Token.StringTemplateHead: @@ -1418,6 +1658,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const directives = parseDirectiveList(); reportInvalidDirective(directives, "expression"); continue; + case Token.HashBrace: + return parseObjectLiteral(); + case Token.HashBracket: + return parseArrayLiteral(); case Token.VoidKeyword: return parseVoidKeyword(); case Token.NeverKeyword: @@ -1494,6 +1738,29 @@ 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 parseArrayLiteral(): ArrayLiteralNode { + const pos = tokenPos(); + const values = parseList(ListKind.ArrayLiteral, parseExpression); + return { + kind: SyntaxKind.ArrayLiteral, + values, + ...finishNode(pos), + }; + } + function parseStringLiteral(): StringLiteralNode { const pos = tokenPos(); const value = tokenValue(); @@ -1795,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 = parseMixedParameterConstraint(); } return { kind: SyntaxKind.FunctionParameter, @@ -3185,6 +3452,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: @@ -3231,6 +3500,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) || @@ -3245,8 +3515,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) || @@ -3270,6 +3543,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) || @@ -3290,6 +3565,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: @@ -3373,7 +3650,14 @@ 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); + case SyntaxKind.ArrayLiteral: + 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/program.ts b/packages/compiler/src/core/program.ts index 328f10266d..de70505b8a 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, @@ -1172,15 +1173,15 @@ export async function compile( } } - function getNode(target: Node | Type | Sym): Node | undefined { - if (!("kind" in target)) { + function getNode(target: Node | Entity | Sym): Node | undefined { + if (!("kind" in target) && !("valueKind" in target) && !("entityKind" 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 b33e3e19b9..7b1770482b 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -2,12 +2,13 @@ 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, isValue } from "./type-utils.js"; import { DecoratorApplication, DecoratorArgument, Enum, EnumMember, + IndeterminateEntity, Interface, Model, ModelProperty, @@ -21,6 +22,7 @@ import { TypeMapper, Union, UnionVariant, + Value, } from "./types.js"; /** @@ -94,7 +96,22 @@ export function createProjector( return projectedProgram; - function projectType(type: Type): Type { + 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 | IndeterminateEntity + ): Type | Value | IndeterminateEntity; + function projectType( + type: Type | Value | IndeterminateEntity + ): Type | Value | IndeterminateEntity { + if (isValue(type)) { + return type; + } + if (type.entityKind === "Indeterminate") { + return { entityKind: "Indeterminate", type: projectType(type.type) as any }; + } if (projectedTypes.has(type)) { return projectedTypes.get(type)!; } @@ -547,7 +564,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" && 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/scanner.ts b/packages/compiler/src/core/scanner.ts index 75dd01ab43..74fd4d5528 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -84,6 +84,8 @@ export enum Token { At, AtAt, Hash, + HashBrace, + HashBracket, Star, ForwardSlash, Plus, @@ -122,7 +124,8 @@ export enum Token { IfKeyword, DecKeyword, FnKeyword, - ValueOfKeyword, + ConstKeyword, + InitKeyword, // Add new statement keyword above /** @internal */ __EndStatementKeyword, @@ -147,6 +150,8 @@ export enum Token { VoidKeyword, NeverKeyword, UnknownKeyword, + ValueOfKeyword, + TypeOfKeyword, // Add new non-statement keyword above /** @internal */ __EndKeyword, @@ -213,6 +218,8 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.At, "'@'"], [Token.AtAt, "'@@'"], [Token.Hash, "'#'"], + [Token.HashBrace, "'#{'"], + [Token.HashBracket, "'#['"], [Token.Star, "'*'"], [Token.ForwardSlash, "'/'"], [Token.Plus, "'+'"], @@ -243,6 +250,9 @@ 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'"], [Token.TrueKeyword, "'true'"], [Token.FalseKeyword, "'false'"], @@ -273,6 +283,9 @@ 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], ["false", Token.FalseKeyword], ["return", Token.ReturnKeyword], @@ -511,7 +524,15 @@ export function createScanner( return lookAhead(1) === CharCode.At ? next(Token.AtAt, 2) : next(Token.At); case CharCode.Hash: - return 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 d6d4a5657c..622a80e6af 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, @@ -95,7 +96,7 @@ export function scopeNavigationToNamespace( return ListenerFlow.NoRecursion; } } - return callback(x as any); + return (callback as any)(x); }; } return wrappedListeners as any; @@ -143,7 +144,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), }; } @@ -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/type-utils.ts b/packages/compiler/src/core/type-utils.ts index f08d370aba..29a3c82f56 100644 --- a/packages/compiler/src/core/type-utils.ts +++ b/packages/compiler/src/core/type-utils.ts @@ -1,5 +1,7 @@ -import { Program } from "./program.js"; +import type { Program } from "./program.js"; import { + ArrayModelType, + Entity, Enum, ErrorType, Interface, @@ -17,27 +19,50 @@ import { Type, TypeMapper, UnknownType, + Value, 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 { + return entity.entityKind === "Type"; +} +export function isValue(entity: Entity): entity is Value { + return entity.entityKind === "Value"; +} + +/** + * @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"); } /** diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 9048d8f73b..133681deed 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1,17 +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 +export type MarshalledValue = +Value extends StringValue ? string + : Value extends NumericValue ? number | Numeric + : Value extends BooleanValue ? boolean + : Value extends ObjectValue ? Record + : Value extends ArrayValue ? unknown[] + : Value extends EnumValue ? EnumMember + : Value extends NullValue ? null + : Value extends ScalarValue ? Value + : Value /** * Type System types @@ -20,17 +26,25 @@ 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 | string | number | boolean; + 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; @@ -42,6 +56,7 @@ export interface DecoratorFunction { } export interface BaseType { + readonly entityKind: "Type"; kind: string; node?: Node; instantiationParameters?: Type[]; @@ -72,9 +87,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 | IndeterminateEntity; + args: readonly (Type | Value | IndeterminateEntity)[]; + /** @internal */ map: Map; } export interface TemplatedTypeBase { @@ -82,34 +97,43 @@ export interface TemplatedTypeBase { /** * @deprecated use templateMapper instead. */ - templateArguments?: Type[]; + templateArguments?: (Type | Value | IndeterminateEntity)[]; 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 | MixedParameterConstraint | IndeterminateEntity; + export type Type = - | Model - | ModelProperty - | Scalar - | Interface + | BooleanLiteral + | Decorator | Enum | EnumMember - | TemplateParameter + | FunctionParameter + | FunctionType + | Interface + | IntrinsicType + | Model + | ModelProperty | Namespace + | NumericLiteral + | ObjectType | Operation + | Projection + | Scalar + | ScalarConstructor | StringLiteral - | NumericLiteral - | BooleanLiteral | StringTemplate | StringTemplateSpan + | TemplateParameter | Tuple | Union - | UnionVariant - | IntrinsicType - | FunctionType - | Decorator - | FunctionParameter - | ObjectType - | Projection; + | UnionVariant; export type StdTypes = { // Models @@ -143,14 +167,33 @@ export interface Projector { parentProjector?: Projector; projections: ProjectionApplication[]; projectedTypes: Map; - projectType(type: Type): Type; + projectType(type: Type | Value): Type | Value; projectedStartNode?: Type; projectedGlobalNamespace?: Namespace; } -export interface ValueType { - kind: "Value"; // Todo remove? - target: Type; +export interface MixedParameterConstraint { + readonly entityKind: "MixedParameterConstraint"; + readonly node?: UnionExpressionNode | Expression; + + /** Type constraints */ + readonly type?: Type; + + /** Expecting value */ + 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 entityKind: "Indeterminate"; + readonly type: + | StringLiteral + | StringTemplate + | NumericLiteral + | BooleanLiteral + | EnumMember + | UnionVariant + | NullType; } export interface IntrinsicType extends BaseType { @@ -232,7 +275,8 @@ export interface Model extends BaseType, DecoratedType, TemplatedTypeBase { | ModelStatementNode | ModelExpressionNode | IntersectionExpressionNode - | ProjectionModelExpressionNode; + | ProjectionModelExpressionNode + | ObjectLiteralNode; namespace?: Namespace; indexer?: ModelIndexer; @@ -295,17 +339,96 @@ 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, // this tracks the property we copied from. sourceProperty?: ModelProperty; optional: boolean; + /** @deprecated use {@link defaultValue} instead. */ default?: Type; + defaultValue?: Value; model?: Model; } +//#region Values +export type Value = + | ScalarValue + | NumericValue + | StringValue + | BooleanValue + | ObjectValue + | ArrayValue + | EnumValue + | NullValue; + +interface BaseValue { + readonly entityKind: "Value"; + readonly valueKind: string; + /** + * 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 { + valueKind: "ObjectValue"; + node: ObjectLiteralNode; + properties: Map; +} + +export interface ObjectValuePropertyDescriptor { + node: ObjectLiteralPropertyNode; + name: string; + value: Value; +} + +export interface ArrayValue extends BaseValue { + valueKind: "ArrayValue"; + node: ArrayLiteralNode; + values: Value[]; +} + +export interface ScalarValue extends BaseValue { + valueKind: "ScalarValue"; + scalar: Scalar; + value: { name: string; args: Value[] }; +} + +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 EnumValue extends BaseValue { + valueKind: "EnumValue"; + value: EnumMember; +} +export interface NullValue extends BaseValue { + valueKind: "NullValue"; + value: null; +} + +//#endregion Values + export interface Scalar extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Scalar"; name: string; @@ -325,13 +448,22 @@ 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; } +export interface ScalarConstructor extends BaseType { + kind: "ScalarConstructor"; + node: ScalarConstructorNode; + name: string; + scalar: Scalar; + parameters: SignatureFunctionParameter[]; +} + export interface Interface extends BaseType, DecoratedType, TemplatedTypeBase { kind: "Interface"; name: string; @@ -489,6 +621,7 @@ export interface NumericLiteral extends BaseType { kind: "Number"; node?: NumericLiteralNode; value: number; + numericValue: Numeric; valueAsString: string; } @@ -500,6 +633,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[]; } @@ -522,7 +657,7 @@ export interface StringTemplateSpanValue extends BaseType { export interface Tuple extends BaseType { kind: "Tuple"; - node: TupleExpressionNode; + node: TupleExpressionNode | ArrayLiteralNode; values: Type[]; } @@ -563,8 +698,8 @@ export interface UnionVariant extends BaseType, DecoratedType { export interface TemplateParameter extends BaseType { kind: "TemplateParameter"; node: TemplateParameterDeclarationNode; - constraint?: Type | ValueType; - default?: Type; + constraint?: MixedParameterConstraint; + default?: Type | Value | IndeterminateEntity; } export interface Decorator extends BaseType { @@ -572,8 +707,8 @@ export interface Decorator extends BaseType { node: DecoratorDeclarationStatementNode; name: `@${string}`; namespace: Namespace; - target: FunctionParameter; - parameters: FunctionParameter[]; + target: MixedFunctionParameter; + parameters: MixedFunctionParameter[]; implementation: (...args: unknown[]) => void; } @@ -582,20 +717,31 @@ 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 | ValueType; optional: boolean; rest: boolean; } +/** Represent a function parameter that could accept types or values in the TypeSpec program. */ +export interface MixedFunctionParameter extends FunctionParameterBase { + mixed: true; + 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 SignatureFunctionParameter extends FunctionParameterBase { + mixed: false; + type: Type; +} +export type FunctionParameter = MixedFunctionParameter | SignatureFunctionParameter; + export interface Sym { readonly flags: SymbolFlags; @@ -657,10 +803,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 | null; } /** @@ -699,27 +848,29 @@ export const enum SymbolFlags { SourceFile = 1 << 21, 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 << 24, + LateBound = 1 << 26, ExportContainer = Namespace | SourceFile, /** * Symbols whose members will be late bound (and stored on the type) */ - MemberContainer = Model | Enum | Union | Interface, - Member = ModelProperty | EnumMember | UnionVariant | InterfaceMember, + MemberContainer = Model | Enum | Union | Interface | Scalar, + Member = ModelProperty | EnumMember | UnionVariant | InterfaceMember | ScalarMember, } /** * 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 | IndeterminateEntity)[]): Type | undefined; + set(args: readonly (Type | Value | IndeterminateEntity)[], type: Type): void; } /** @@ -835,6 +986,14 @@ export enum SyntaxKind { Return, JsNamespaceDeclaration, TemplateArgument, + TypeOfExpression, + ObjectLiteral, + ObjectLiteralProperty, + ObjectLiteralSpreadProperty, + ArrayLiteral, + ConstStatement, + CallExpression, + ScalarConstructor, } export const enum NodeFlags { @@ -930,7 +1089,12 @@ export type Node = | ProjectionModelPropertyNode | ProjectionModelSpreadPropertyNode | ProjectionStatementNode - | ProjectionNode; + | ProjectionNode + | ObjectLiteralNode + | ObjectLiteralPropertyNode + | ObjectLiteralSpreadPropertyNode + | ScalarConstructorNode + | ArrayLiteralNode; /** * Node that can be used as template @@ -957,9 +1121,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. @@ -1015,6 +1180,8 @@ export type Statement = | DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode | AugmentDecoratorStatementNode + | ConstStatementNode + | CallExpressionNode | EmptyStatementNode | InvalidStatementNode | ProjectionStatementNode; @@ -1036,6 +1203,7 @@ export type Declaration = | ProjectionLambdaParameterDeclarationNode | EnumStatementNode | AliasStatementNode + | ConstStatementNode | DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode; @@ -1086,11 +1254,15 @@ export type Expression = | ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode + | ObjectLiteralNode + | ArrayLiteralNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode + | TypeOfExpressionNode + | CallExpressionNode | StringLiteralNode | NumericLiteralNode | BooleanLiteralNode @@ -1185,9 +1357,17 @@ 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; } +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[]; @@ -1237,6 +1417,18 @@ 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 CallExpressionNode extends BaseNode { + readonly kind: SyntaxKind.CallExpression; + readonly target: MemberExpressionNode | IdentifierNode; + readonly arguments: Expression[]; +} + export interface InvalidStatementNode extends BaseNode { readonly kind: SyntaxKind.InvalidStatement; readonly decorators: readonly DecoratorExpressionNode[]; @@ -1276,6 +1468,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: Expression; + readonly parent?: ObjectLiteralNode; +} + +export interface ObjectLiteralSpreadPropertyNode extends BaseNode { + readonly kind: SyntaxKind.ObjectLiteralSpreadProperty; + readonly target: TypeReferenceNode; + readonly parent?: ObjectLiteralNode; +} + +export interface ArrayLiteralNode extends BaseNode { + readonly kind: SyntaxKind.ArrayLiteral; + readonly values: readonly Expression[]; +} + export type LiteralNode = | StringLiteralNode | NumericLiteralNode @@ -1369,6 +1584,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; @@ -1783,23 +2003,29 @@ export type LocationContext = /** Defined in the user project. */ export interface ProjectLocationContext { - type: "project"; + readonly type: "project"; + readonly flags?: PackageFlags; } /** 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?: PackageFlags; } export interface LibraryInstance { @@ -1834,10 +2060,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 { @@ -1863,7 +2089,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"; @@ -2173,6 +2399,25 @@ export interface TypeSpecLibraryDef< readonly state?: Record; } +export interface PackageFlags { + /** + * Decorator arg marshalling algorithm. Specify how TypeSpec values are marshalled to decorator arguments. + * - `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` + * - 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" | "new"; +} + export interface LinterDefinition { rules: LinterRuleDefinition[]; ruleSets?: Record; diff --git a/packages/compiler/src/emitter-framework/type-emitter.ts b/packages/compiler/src/emitter-framework/type-emitter.ts index ef2bee27cf..dbabf65a26 100644 --- a/packages/compiler/src/emitter-framework/type-emitter.ts +++ b/packages/compiler/src/emitter-framework/type-emitter.ts @@ -780,6 +780,12 @@ export class TypeEmitter> { let unspeakable = false; const parameterNames = declarationType.templateMapper.args.map((t) => { + if (t.entityKind === "Indeterminate") { + t = t.type; + } + if (!("kind" in t)) { + return undefined; + } switch (t.kind) { case "Model": case "Scalar": 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 1437a7e43e..50efdb47c8 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -6,10 +6,13 @@ import { Keywords } from "../../core/scanner.js"; import { AliasStatementNode, ArrayExpressionNode, + ArrayLiteralNode, AugmentDecoratorStatementNode, BlockComment, BooleanLiteralNode, + CallExpressionNode, Comment, + ConstStatementNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, DirectiveExpressionNode, @@ -31,6 +34,9 @@ import { Node, NodeFlags, NumericLiteralNode, + ObjectLiteralNode, + ObjectLiteralPropertyNode, + ObjectLiteralSpreadPropertyNode, OperationSignatureDeclarationNode, OperationSignatureReferenceNode, OperationStatementNode, @@ -55,6 +61,7 @@ import { ProjectionTupleExpressionNode, ProjectionUnaryExpressionNode, ReturnExpressionNode, + ScalarConstructorNode, ScalarStatementNode, Statement, StringLiteralNode, @@ -65,6 +72,7 @@ import { TemplateParameterDeclarationNode, TextRange, TupleExpressionNode, + TypeOfExpressionNode, TypeReferenceNode, TypeSpecScriptNode, UnionExpressionNode, @@ -166,6 +174,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: @@ -215,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, @@ -368,6 +380,22 @@ 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.ArrayLiteral: + return printArrayLiteral(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: @@ -399,6 +427,7 @@ export function printTypeSpecScript( body.push(printStatementSequence(path, options, print, "statements")); return body; } + export function printAliasStatement( path: AstPath, options: TypeSpecPrettierOptions, @@ -409,6 +438,26 @@ 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"), ";"]; +} + +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, @@ -559,7 +608,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]; } @@ -622,8 +671,8 @@ export function printDirective( return ["#", path.call(print, "target"), " ", args]; } -function printDecoratorArgs( - path: AstPath, +function printCallOrDecoratorArgs( + path: AstPath, options: TypeSpecPrettierOptions, print: PrettierChildPrint ) { @@ -965,6 +1014,63 @@ 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 printArrayLiteral( + 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, @@ -1077,9 +1183,12 @@ function shouldWrapMemberInNewLines( | ModelSpreadPropertyNode | EnumMemberNode | EnumSpreadMemberNode + | ScalarConstructorNode | UnionVariantNode | ProjectionModelPropertyNode | ProjectionModelSpreadPropertyNode + | ObjectLiteralPropertyNode + | ObjectLiteralSpreadPropertyNode >, options: any ): boolean { @@ -1088,6 +1197,9 @@ 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, { tryInline: DecoratorsTryInline.modelProperty, })) || @@ -1185,7 +1297,7 @@ function isModelExpressionInBlock( } } -export function printScalarStatement( +function printScalarStatement( path: AstPath, options: TypeSpecPrettierOptions, print: PrettierChildPrint @@ -1197,16 +1309,58 @@ 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( path: AstPath, options: TypeSpecPrettierOptions, @@ -1373,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/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index d367194ab8..e7d42e421d 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, @@ -43,17 +43,39 @@ import { } from "../core/decorator-utils.js"; import { getDeprecationDetails, markDeprecated } from "../core/deprecation.js"; import { + Numeric, StdTypeName, 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, + setMinItems, + setMinLength, + setMinValue, + setMinValueExclusive, +} from "../core/intrinsic-type-state.js"; import { createDiagnostic, reportDiagnostic } from "../core/messages.js"; import { Program, ProjectedProgram } from "../core/program.js"; import { - ArrayModelType, DecoratorContext, Enum, EnumMember, @@ -114,24 +136,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. * @@ -153,57 +157,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 @@ -357,21 +310,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. */ @@ -512,62 +450,47 @@ export function getPatternData(program: Program, target: Type): PatternData | un // -- @minLength decorator --------------------- -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; } - - context.program.stateMap(minLengthValuesKey).set(target, minLength); + setMinLength(context.program, target, minLength); }; -export function getMinLength(program: Program, target: Type): number | undefined { - return program.stateMap(minLengthValuesKey).get(target); -} - // -- @maxLength decorator --------------------- -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; } - context.program.stateMap(maxLengthValuesKey).set(target, maxLength); + setMaxLength(context.program, target, maxLength); }; -export function getMaxLength(program: Program, target: Type): number | undefined { - return program.stateMap(maxLengthValuesKey).get(target); -} - // -- @minItems decorator --------------------- -const minItemsValuesKey = createStateSymbol("minItems"); - export const $minItems: MinItemsDecorator = ( context: DecoratorContext, target: Type, - minItems: number + minItems: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $minItems); @@ -582,25 +505,19 @@ 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); + setMinItems(context.program, target, minItems); }; -export function getMinItems(program: Program, target: Type): number | undefined { - return program.stateMap(minItemsValuesKey).get(target); -} - // -- @maxLength decorator --------------------- -const maxItemsValuesKey = createStateSymbol("maxItems"); - export const $maxItems: MaxItemsDecorator = ( context: DecoratorContext, target: Type, - maxItems: number + maxItems: Numeric ) => { validateDecoratorUniqueOnNode(context, target, $maxItems); @@ -614,25 +531,19 @@ 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); + setMaxItems(context.program, target, maxItems); }; -export function getMaxItems(program: Program, target: Type): number | undefined { - return program.stateMap(maxItemsValuesKey).get(target); -} - // -- @minValue decorator --------------------- -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); @@ -646,26 +557,21 @@ export const $minValue: MinValueDecorator = ( !validateRange( context, minValue, - getMaxValue(context.program, target) ?? getMaxValueExclusive(context.program, target) + getMaxValueAsNumeric(context.program, target) ?? + getMaxValueExclusiveAsNumeric(context.program, target) ) ) { return; } - program.stateMap(minValuesKey).set(target, minValue); + setMinValue(program, target, minValue); }; -export function getMinValue(program: Program, target: Type): number | undefined { - return program.stateMap(minValuesKey).get(target); -} - // -- @maxValue decorator --------------------- -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); @@ -677,27 +583,22 @@ export const $maxValue: MaxValueDecorator = ( if ( !validateRange( context, - getMinValue(context.program, target) ?? getMinValueExclusive(context.program, target), + getMinValueAsNumeric(context.program, target) ?? + getMinValueExclusiveAsNumeric(context.program, target), maxValue ) ) { return; } - program.stateMap(maxValuesKey).set(target, maxValue); + setMaxValue(program, target, maxValue); }; -export function getMaxValue(program: Program, target: Type): number | undefined { - return program.stateMap(maxValuesKey).get(target); -} - // -- @minValueExclusive decorator --------------------- -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); @@ -711,26 +612,21 @@ export const $minValueExclusive: MinValueExclusiveDecorator = ( !validateRange( context, minValueExclusive, - getMaxValue(context.program, target) ?? getMaxValueExclusive(context.program, target) + getMaxValueAsNumeric(context.program, target) ?? + getMaxValueExclusiveAsNumeric(context.program, target) ) ) { return; } - program.stateMap(minValueExclusiveKey).set(target, minValueExclusive); + setMinValueExclusive(program, target, minValueExclusive); }; -export function getMinValueExclusive(program: Program, target: Type): number | undefined { - return program.stateMap(minValueExclusiveKey).get(target); -} - // -- @maxValueExclusive decorator --------------------- -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); @@ -742,19 +638,15 @@ export const $maxValueExclusive: MaxValueExclusiveDecorator = ( if ( !validateRange( context, - getMinValue(context.program, target) ?? getMinValueExclusive(context.program, target), + getMinValueAsNumeric(context.program, target) ?? + getMinValueExclusiveAsNumeric(context.program, target), maxValueExclusive ) ) { return; } - program.stateMap(maxValueExclusiveKey).set(target, maxValueExclusive); + setMaxValueExclusive(program, target, maxValueExclusive); }; - -export function getMaxValueExclusive(program: Program, target: Type): number | undefined { - return program.stateMap(maxValueExclusiveKey).get(target); -} - // -- @secret decorator --------------------- const secretTypesKey = createStateSymbol("secretTypes"); @@ -1002,7 +894,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 --------------------- @@ -1430,14 +1326,13 @@ 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 > max) { + if (min.gt(max)) { reportDiagnostic(context.program, { code: "invalid-range", format: { start: min.toString(), end: max.toString() }, diff --git a/packages/compiler/src/lib/intrinsic-decorators.ts b/packages/compiler/src/lib/intrinsic-decorators.ts index 6df1b9904c..688e783cf2 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-state.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/classify.ts b/packages/compiler/src/server/classify.ts index 5a93c6ae50..9693221f8a 100644 --- a/packages/compiler/src/server/classify.ts +++ b/packages/compiler/src/server/classify.ts @@ -201,6 +201,7 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] { classify(node.id, SemanticTokenKind.TypeParameter); break; case SyntaxKind.ModelProperty: + case SyntaxKind.ObjectLiteralProperty: case SyntaxKind.UnionVariant: if (node.id) { classify(node.id, SemanticTokenKind.Property); @@ -215,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; @@ -239,6 +243,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; @@ -249,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/completion.ts b/packages/compiler/src/server/completion.ts index c1f5449c3e..92d9cbd856 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -55,6 +55,9 @@ export async function resolveCompletion( case SyntaxKind.NamespaceStatement: addKeywordCompletion("namespace", context.completions); break; + case SyntaxKind.ScalarStatement: + addKeywordCompletion("scalar", context.completions); + break; case SyntaxKind.Identifier: addDirectiveCompletion(context, node); addIdentifierCompletion(context, node); @@ -75,6 +78,7 @@ interface KeywordArea { namespace?: boolean; model?: boolean; identifier?: boolean; + scalar?: boolean; } const keywords = [ @@ -93,6 +97,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 }], @@ -107,6 +112,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/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 97a335c214..4487eacc57 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"; @@ -548,7 +548,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/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index bd39538e1a..37a1ebe181 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -51,7 +51,9 @@ export type TypeSpecScope = | "punctuation.curlybrace.open.tsp" | "punctuation.curlybrace.close.tsp" | "punctuation.parenthesis.open.tsp" - | "punctuation.parenthesis.close.tsp"; + | "punctuation.parenthesis.close.tsp" + | "punctuation.hashcurlybrace.open.tsp" + | "punctuation.hashsquarebracket.open.tsp"; const meta: typeof tm.meta = tm.meta; const identifierStart = "[_$[:alpha:]]"; @@ -61,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`; @@ -265,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, @@ -305,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, @@ -317,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 = { @@ -367,6 +408,20 @@ const typeParameters: BeginEndRule = { patterns: [typeParameter, punctuationComma], }; +const tupleLiteral: BeginEndRule = { + key: "tuple-literal", + scope: meta, + begin: "#\\[", + beginCaptures: { + "0": { scope: "punctuation.hashsquarebracket.open.tsp" }, + }, + end: "\\]", + endCaptures: { + "0": { scope: "punctuation.squarebracket.close.tsp" }, + }, + patterns: [expression, punctuationComma], +}; + const tupleExpression: BeginEndRule = { key: "tuple-expression", scope: meta, @@ -451,6 +506,32 @@ const modelExpression: BeginEndRule = { ], }; +const objectLiteralProperty: BeginEndRule = { + key: "object-literal-property", + scope: meta, + begin: `(?:(${identifier})\\s*(:))`, + beginCaptures: { + "1": { scope: "variable.name.tsp" }, + "2": { scope: "keyword.operator.type.annotation.tsp" }, + }, + end: universalEnd, + patterns: [token, expression], +}; + +const objectLiteral: BeginEndRule = { + key: "object-literal", + scope: meta, + begin: "#\\{", + beginCaptures: { + "0": { scope: "punctuation.hashcurlybrace.open.tsp" }, + }, + end: "\\}", + endCaptures: { + "0": { scope: "punctuation.curlybrace.close.tsp" }, + }, + patterns: [token, objectLiteralProperty, directive, spreadExpression, punctuationComma], +}; + const modelHeritage: BeginEndRule = { key: "model-heritage", scope: meta, @@ -478,6 +559,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, @@ -489,19 +584,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, + 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, ], }; @@ -581,15 +703,39 @@ 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: [aliasAssignment, typeParameters], +}; + +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: [typeParameters, operatorAssignment, expression], + patterns: [typeAnnotation, operatorAssignment, expression], }; const namespaceName: BeginEndRule = { @@ -625,20 +771,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, @@ -921,9 +1053,13 @@ expression.patterns = [ directive, parenthesizedExpression, valueOfExpression, + typeOfExpression, typeArguments, + objectLiteral, + tupleLiteral, tupleExpression, modelExpression, + callExpression, identifierExpression, ]; @@ -938,6 +1074,7 @@ statement.patterns = [ interfaceStatement, enumStatement, aliasStatement, + constStatement, namespaceStatement, operationStatement, importStatement, 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/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index c1979b8080..9c650f6725 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 { @@ -15,7 +15,7 @@ import { SyntaxKind, Type, UnionVariant, - ValueType, + Value, } from "../core/types.js"; import { printId } from "../formatter/print/printer.js"; @@ -26,11 +26,22 @@ 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 getTypeSignature(type: Type | ValueType): string { +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 { switch (type.kind) { case "Scalar": case "Enum": @@ -39,15 +50,14 @@ 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": 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}"`)}`; @@ -106,7 +116,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/augment-decorators.test.ts b/packages/compiler/test/checker/augment-decorators.test.ts index abfc3ef874..05ef006362 100644 --- a/packages/compiler/test/checker/augment-decorators.test.ts +++ b/packages/compiler/test/checker/augment-decorators.test.ts @@ -236,12 +236,14 @@ 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, }); 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/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 66947347ea..ecf58ed479 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -1,6 +1,8 @@ -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 { PackageFlags, isNullType, setTypeSpecNamespace } from "../../src/core/index.js"; +import { numericRanges } from "../../src/core/numeric-ranges.js"; +import { Numeric } from "../../src/core/numeric.js"; import { BasicTestRunner, TestHost, @@ -8,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; @@ -99,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: PackageFlags; beforeEach(() => { + $flags = {}; calledArgs = undefined; testHost.addJsFile("test.js", { + $flags, $testDec: (...args: any[]) => (calledArgs = args), }); runner = createTestWrapper(testHost, { @@ -261,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(); }); @@ -277,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(); @@ -303,10 +323,16 @@ 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 { + mutate($flags).decoratorArgMarshalling = "new"; await runner.compile(` extern dec testDec(target: unknown, arg1: ${type}); - + + ${suppress ? `#suppress "deprecated" "for testing"` : ""} @testDec(${value}) @test model Foo {} @@ -342,18 +368,254 @@ 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); + 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", + 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", () => { + 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("sends null", async () => { + const arg = await testCallDecorator("valueof null", `null`); + strictEqual(arg, null); + }); + }); + + 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" }); + }); + + 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 array value", () => { + 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"]]); + }); + }); + + // 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"]]); + }); + }); + }); + }); + + 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 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" }); + }); + + 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 array value", () => { + 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"]]); + }); + }); }); }); diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index af978cd9b1..5d0ccba8fb 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,17 +109,102 @@ describe("compiler: models", () => { ]); }); - 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 }], - ]; + describe("property defaults", () => { + describe("set defaultValue", () => { + 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" }], + ["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", + ` + model A { @test foo?: ${type} = ${defaultValue} } + ` + ); + const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; + strictEqual(foo.defaultValue?.valueKind, expectedValue.kind); + expect((foo.defaultValue as any).value).toMatchObject(expectedValue.value); + }); - for (const [type, defaultValue, expectedValue] of testCases) { - it(`foo?: ${type} = ${defaultValue}`, async () => { + 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 }; + 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(`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", + ` + model A { @test foo?: TestEnum = TestEnum.up } + enum TestEnum {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"); + }); + }); + + 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", ` @@ -121,9 +212,34 @@ describe("compiler: models", () => { ` ); const { foo } = (await testHost.compile("main.tsp")) as { foo: ModelProperty }; - deepStrictEqual({ ...foo.default }, expectedValue); + // eslint-disable-next-line deprecation/deprecation + 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 }; + // eslint-disable-next-line deprecation/deprecation + 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 }; + // eslint-disable-next-line deprecation/deprecation + deepStrictEqual(foo.default, undefined); + }); + }); }); describe("doesn't allow a default of different type than the property type", () => { @@ -131,7 +247,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"'`], ]; @@ -161,13 +277,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", ` @@ -459,8 +575,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/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index 20b13217a5..336fcd1952 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -1,9 +1,16 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Diagnostic, FunctionParameterNode, Model, Type } from "../../src/core/index.js"; +import { + Diagnostic, + FunctionParameterNode, + Model, + Type, + definePackageFlags, +} from "../../src/core/index.js"; import { BasicTestRunner, DiagnosticMatch, + TestHost, createTestHost, createTestWrapper, expectDiagnosticEmpty, @@ -19,9 +26,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 +37,12 @@ describe("compiler: checker: type relations", () => { diagnostics: readonly Diagnostic[]; expectedDiagnosticPos: number; }> { + host.addJsFile("mock.js", { + $flags: definePackageFlags({ + decoratorArgMarshalling: "new", + }), + $mock: () => null, + }); const { source: code, pos } = extractCursor(` import "./mock.js"; ${commonCode ?? ""} @@ -50,6 +63,26 @@ describe("compiler: checker: type relations", () => { return { related, diagnostics, expectedDiagnosticPos: pos }; } + async function checkValueAssignableToConstraint({ + source, + target, + commonCode, + }: RelatedTypeOptions): Promise<{ + related: boolean; + diagnostics: readonly Diagnostic[]; + expectedDiagnosticPos: number; + }> { + const cursor = source.includes("┆") ? "" : "┆"; + const { source: code, pos } = extractCursor(` + ${commonCode ?? ""} + model Test {} + alias Case = Test<${cursor}${source}>; + `); + + const diagnostics = await runner.diagnose(code); + return { related: diagnostics.length === 0, diagnostics, expectedDiagnosticPos: pos }; + } + async function expectTypeAssignable(options: RelatedTypeOptions) { const { related, diagnostics } = await checkTypeAssignable(options); expectDiagnosticEmpty(diagnostics); @@ -62,6 +95,22 @@ describe("compiler: checker: type relations", () => { expectDiagnostics(diagnostics, { ...match, pos: expectedDiagnosticPos }); } + 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 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 }); + } + describe("model with indexer", () => { it("can add property of subtype of indexer", async () => { const diagnostics = await runner.diagnose(` @@ -280,11 +329,51 @@ 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"` }); }); + 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"` }, @@ -306,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" }); @@ -399,15 +501,14 @@ 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" }); }); - 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 () => { @@ -520,7 +621,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'", } ); }); @@ -609,6 +710,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 () => { @@ -742,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.", } ); }); @@ -752,8 +915,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 }'`, } ); }); @@ -817,12 +979,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("can assign a tuple of subtype", async () => { - await expectTypeAssignable({ source: "[int32, int32, int32]", target: "numeric[]" }); + it("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 () => { @@ -849,8 +1047,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[]'", } ); }); @@ -1017,8 +1215,8 @@ describe("compiler: checker: type relations", () => { `); expectDiagnostics(diagnostics, { - code: "missing-property", - message: `Property 'a' is missing on type '(anonymous model)' but required in '(anonymous model)'`, + code: "invalid-argument", + message: `Argument of type '{ b: string }' is not assignable to parameter of type '{ a: string }'`, }); }); @@ -1089,18 +1287,19 @@ 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" }); + await checkValueAssignableToConstraint({ source: `"foo bar"`, target: "string" }); }); it("cannot assign numeric literal", async () => { - await expectTypeNotAssignable( + 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 'valueof string'", } ); }); @@ -1118,15 +1317,16 @@ describe("compiler: checker: type relations", () => { describe("valueof boolean", () => { it("can assign boolean literal", async () => { - await expectTypeAssignable({ source: `true`, target: "valueof boolean" }); + await expectValueAssignableToConstraint({ source: `true`, target: "valueof boolean" }); }); it("cannot assign numeric literal", async () => { - await expectTypeNotAssignable( + 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 'valueof boolean'", } ); }); @@ -1144,7 +1344,7 @@ describe("compiler: checker: type relations", () => { describe("valueof int16", () => { it("can assign int16 literal", async () => { - await expectTypeAssignable({ source: `12`, target: "valueof int16" }); + await expectValueAssignableToConstraint({ source: `12`, target: "valueof int16" }); }); it("can assign valueof int8", async () => { @@ -1152,31 +1352,33 @@ describe("compiler: checker: type relations", () => { }); it("cannot assign int too large", async () => { - await expectTypeNotAssignable( + 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 'valueof int16'", } ); }); it("cannot assign float", async () => { - await expectTypeNotAssignable( + 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 'valueof int16'", } ); }); it("cannot assign string literal", async () => { - await expectTypeNotAssignable( + 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 'valueof int16'`, } ); }); @@ -1194,15 +1396,15 @@ describe("compiler: checker: type relations", () => { describe("valueof float32", () => { it("can assign float32 literal", async () => { - await expectTypeAssignable({ source: `12.6`, target: "valueof float32" }); + await expectValueAssignableToConstraint({ source: `12.6`, target: "valueof float32" }); }); it("cannot assign string literal", async () => { - await expectTypeNotAssignable( + 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 'valueof float32'`, } ); }); @@ -1218,6 +1420,202 @@ describe("compiler: checker: type relations", () => { }); }); + describe("valueof model", () => { + it("can assign object value", async () => { + await expectValueAssignableToConstraint({ + source: `#{name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }); + }); + + it("can assign object value with optional properties", async () => { + await expectValueAssignableToConstraint({ + source: `#{name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { name: string, age?: int32 }`, + }); + }); + + it("can assign object value with additional properties", async () => { + await expectValueAssignableToConstraint({ + source: `#{age: 21, name: "foo"}`, + target: "valueof Info", + commonCode: `model Info { age: int32, ...Record }`, + }); + }); + + // Disabled for now as this is allowed for backcompat + it.skip("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'", + } + ); + }); + + describe("excess properties", () => { + it("emit diagnostic when using extra properties", async () => { + await expectValueNotAssignableToConstraint( + { + source: `#{name: "foo", notDefined: "bar"}`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }, + { + code: "invalid-argument", + message: `Argument of type '#{name: "foo", notDefined: "bar"}' is not assignable to parameter of type 'valueof Info'`, + } + ); + }); + + it("don't emit diagnostic when the extra props are spread into it", async () => { + await expectValueAssignableToConstraint({ + source: `#{name: "foo", ...common}`, + target: "valueof Info", + commonCode: ` + const common = #{notDefined: "bar"}; + model Info { name: string } + `, + }); + }); + }); + + it("cannot assign a array value", async () => { + await expectValueNotAssignableToConstraint( + { + source: `#["foo"]`, + target: "valueof Info", + commonCode: `model Info { name: string }`, + }, + { + code: "invalid-argument", + message: `Argument of type '#["foo"]' is not assignable to parameter of type 'valueof 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 'valueof Info'", + } + ); + }); + }); + + describe("valueof array", () => { + it("can assign array value", async () => { + await expectValueAssignableToConstraint({ + source: `#["foo"]`, + target: "valueof string[]", + }); + }); + + it("can assign array value of object value", async () => { + await expectValueAssignableToConstraint({ + source: `#[#{name: "a"}, #{name: "b"}]`, + target: "valueof Info[]", + commonCode: `model Info { name: string }`, + }); + }); + + // Disabled for now as this is allowed for backcompat + it.skip("cannot assign a tuple", async () => { + await expectValueNotAssignableToConstraint( + { + source: `["foo"]`, + target: "valueof string[]", + }, + { + code: "unassignable", + message: `Type '["foo"]' is not assignable to type 'valueof string[]'`, + } + ); + }); + + it("cannot assign an object value", async () => { + await expectValueNotAssignableToConstraint( + { + source: `#{name: "foo"}`, + target: "valueof string[]", + }, + { + code: "invalid-argument", + message: `Argument of type '#{name: "foo"}' is not assignable to parameter of type 'valueof string[]'`, + } + ); + }); + + it("cannot assign string scalar", async () => { + await expectTypeNotAssignable( + { source: `string`, target: "valueof string[]" }, + { + code: "unassignable", + message: "Type 'string' is not assignable to type 'valueof string[]'", + } + ); + }); + }); + + describe("valueof tuple", () => { + it("can assign array value", async () => { + await expectValueAssignableToConstraint({ + source: `#["foo", 12]`, + target: "valueof [string, int32]", + }); + }); + + it("cannot assign array value with too few values", async () => { + await expectValueNotAssignableToConstraint( + { + source: `#["foo"]`, + target: "valueof [string, string]", + }, + { + code: "invalid-argument", + message: `Argument of type '#["foo"]' is not assignable to parameter of type 'valueof [string, string]'`, + } + ); + }); + + it("cannot assign array value with too many values", async () => { + await expectValueNotAssignableToConstraint( + { + source: `#["a", "b", "c"]`, + target: "valueof [string, string]", + }, + { + code: "invalid-argument", + message: `Argument of type '#["a", "b", "c"]' is not assignable to parameter of type 'valueof [string, string]'`, + } + ); + }); + }); + + describe("valueof union", () => { + it("can assign array value variant", async () => { + await expectValueAssignableToConstraint({ + source: `#["foo", 12]`, + target: "valueof ([string, int32] | string | boolean)", + }); + }); + it("can assign string variant", async () => { + await expectValueAssignableToConstraint({ + source: `"foo"`, + target: "valueof ([string, int32] | string | boolean)", + }); + }); + }); + it("can use valueof in template parameter constraints", async () => { const diagnostics = await runner.diagnose(` model Foo { @@ -1227,6 +1625,29 @@ 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 {} + model B is A<┆T> {}`); + const diagnostics = await runner.diagnose(source); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: "Argument of type 'T' is not assignable to parameter of 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(` @@ -1241,4 +1662,48 @@ describe("compiler: checker: type relations", () => { }); }); }); + + /** Describe the relation between types and values in TypeSpec */ + describe("value vs type constraints", () => { + describe("cannot assign a value to a type constraint", () => { + it.each([ + ["#{}", "{}"], + ["#{}", "unknown"], + ["#[]", "unknown[]"], + ["#[]", "unknown"], + ])(`%s => %s`, async (source, target) => { + await expectValueNotAssignableToConstraint( + { source, target }, + { code: "invalid-argument" } + ); + }); + }); + + // 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 {}"], + ])(`%s => %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 {}) | {}"], + ])(`%s => %s`, async (source, target) => { + await expectValueAssignableToConstraint({ source, target }); + }); + it.each([ + ["{}", "(valueof unknown) | unknown"], + ["{}", "(valueof {}) | {}"], + ["(valueof {}) | {}", "(valueof {}) | {} | (valueof []) | []"], + ["(valueof {}) | {}", "(valueof {}) | {}"], + ])(`%s => %s`, async (source, target) => { + await expectTypeAssignable({ source, target }); + }); + }); + }); }); diff --git a/packages/compiler/test/checker/scalar.test.ts b/packages/compiler/test/checker/scalar.test.ts index 7c03b88799..10caf9e1e0 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.asNumber(), 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/string-template.test.ts b/packages/compiler/test/checker/string-template.test.ts index 2f0a068032..800052edd3 100644 --- a/packages/compiler/test/checker/string-template.test.ts +++ b/packages/compiler/test/checker/string-template.test.ts @@ -1,74 +1,139 @@ 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, + extractSquiggles, +} 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 "); + + 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"); +}); + +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 "); - 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, "Model"); + strictEqual(template.spans[1].type.name, "TestModel"); - 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("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.", }); +}); - 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 "); +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.", + }); + }); +}); - 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 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, + }); +}); - strictEqual(template.spans[2].isInterpolated, false); - strictEqual(template.spans[2].type.value, " 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, }); }); diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 1b5ffc97d7..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", @@ -226,6 +243,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(` @@ -239,8 +274,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 +314,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 +516,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 +543,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/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/checker/valueof-casting.test.ts b/packages/compiler/test/checker/valueof-casting.test.ts new file mode 100644 index 0000000000..b4a4218e05 --- /dev/null +++ b/packages/compiler/test/checker/valueof-casting.test.ts @@ -0,0 +1,54 @@ +import { ok, strictEqual } from "assert"; +import { it } from "vitest"; +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"`); + 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)').", + }); +}); + +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/checker/values/array-values.test.ts b/packages/compiler/test/checker/values/array-values.test.ts new file mode 100644 index 0000000000..ebdb75147e --- /dev/null +++ b/packages/compiler/test/checker/values/array-values.test.ts @@ -0,0 +1,146 @@ +import { ok, strictEqual } from "assert"; +import { describe, expect, it } from "vitest"; +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(`#[]`); + strictEqual(object.valueKind, "ArrayValue"); + strictEqual(object.values.length, 0); +}); + +it("single value", async () => { + const object = await compileValue(`#["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 compileValue(`#["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.asNumber(), 21); +}); + +describe("valid property types", () => { + it.each([ + ["StringValue", `"John"`], + ["NumericValue", "21"], + ["BooleanValue", "true"], + ["NullValue", "null"], + ["EnumValue", "Direction.up", "enum Direction { up, down }"], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], + ])("%s", async (kind, type, other?) => { + const object = await compileValue(`#[${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 diagnoseValue(`#[{ thisIsAModel: true }]`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: + "{ thisIsAModel: true } refers to a model type, but is being used as a value here. Use #{} to create an object value.", + }); +}); + +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("(LEGACY) cast tuple to array value", () => { + it("create the value", async () => { + const value = await compileValueOrType(`valueof string[]`, `["foo", "bar"]`); + ok(value && isValue(value)); + 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(` + model Test {} + alias A = Test<┆["foo"]>; + `); + + expectDiagnostics(diagnostics, { + code: "deprecated", + message: + "Deprecated: Using a tuple as a value is deprecated. Use an array value instead(with #[]).", + 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: "invalid-argument", + message: + "Argument of type '[string]' is not assignable to parameter of 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/boolean-values.test.ts b/packages/compiler/test/checker/values/boolean-values.test.ts new file mode 100644 index 0000000000..dd5a593425 --- /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 { compileValue, diagnoseValue } from "./utils.js"; + +describe("instantiate with constructor", () => { + it("with boolean literal", async () => { + const value = await compileValue(`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 compileValue(`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 compileValue(`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 diagnoseValue( + `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 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 new file mode 100644 index 0000000000..c80749d808 --- /dev/null +++ b/packages/compiler/test/checker/values/const.test.ts @@ -0,0 +1,73 @@ +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"; + +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 compileValue("a", `const a = ${input};`); + strictEqual(value.type.kind, kind); + }); +}); + +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"); + 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); + 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/numeric-values.test.ts b/packages/compiler/test/checker/values/numeric-values.test.ts new file mode 100644 index 0000000000..9296ecafb1 --- /dev/null +++ b/packages/compiler/test/checker/values/numeric-values.test.ts @@ -0,0 +1,365 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValue, diagnoseUsage, diagnoseValue } from "./utils.js"; + +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 compileValue(`${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("implicit type", () => { + describe("instantiate when type is scalar", () => { + it.each(numericScalars)("%s", async (scalarName) => { + const value = await compileValue(`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("doesn't pick scalar if const has no type", async () => { + const value = await compileValue(`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 compileValue(`a`, `const a: int32 | string = 123;`); + strictEqual(value.valueKind, "NumericValue"); + 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 diagnoseValue(`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", () => { + 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"], https://github.com/microsoft/typespec/issues/3156 + // ["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 compileValue(`${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 compileValue(`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); + }); + + 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 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 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 new file mode 100644 index 0000000000..2cfa75562d --- /dev/null +++ b/packages/compiler/test/checker/values/object-values.test.ts @@ -0,0 +1,197 @@ +import { ok, strictEqual } from "assert"; +import { describe, expect, it } from "vitest"; +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(`#{}`); + strictEqual(object.valueKind, "ObjectValue"); + strictEqual(object.properties.size, 0); +}); + +it("single property", async () => { + const object = await compileValue(`#{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 compileValue(`#{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 compileValue(`#{...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 compileValue( + `#{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 compileValue( + `#{...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 diagnoseValue( + `#{...Common, age: 21}`, + `alias Common = { name: "John" };` + ); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: `{ name: "John" } 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 diagnoseValue(`#{...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"], + ["NullValue", "null"], + ["EnumValue", "Direction.up", "enum Direction { up, down }"], + ["ObjectValue", `#{nested: "foo"}`], + ["ArrayValue", `#["foo"]`], + ])("%s", async (kind, type, other?) => { + const object = await compileValue(`#{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 diagnoseValue(`#{ prop: { thisIsAModel: true }}`); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: + "{ thisIsAModel: true } refers to a model type, but is being used as a value here. Use #{} to create an object value.", + }); +}); + +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("(LEGACY) cast model to object value", () => { + it("create the value", async () => { + 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); + 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 value 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: "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/scalar-values.test.ts b/packages/compiler/test/checker/values/scalar-values.test.ts new file mode 100644 index 0000000000..cb01e8ea20 --- /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 { compileValue, diagnoseValue } 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 compileValue(`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 compileValue(`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 compileValue( + `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 diagnoseValue(`ipv4.fromString(123)`, ipv4Code); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: "Argument of type '123' is not assignable to parameter of type 'string'", + }); + }); + + it("emit warning if passing too many args", async () => { + const diagnostics = await diagnoseValue(`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 diagnoseValue(`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 compileValue( + `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 compileValue( + `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 diagnoseValue( + `ipv4.fromItems("a", 123)`, + ` + scalar ipv4 { + init fromItems(...value: string[]); + } + ` + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: "Argument of type '123' is not assignable to parameter of type 'string'", + }); + }); + }); + describe("with rest params", () => { + it("support rest params", async () => { + const value = await compileValue( + `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 compileValue( + `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 diagnoseValue( + `ipv4.fromItems(123)`, + ` + scalar ipv4 { + init fromItems(...value: string[]); + } + ` + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: "Argument of type '123' is not assignable to parameter of type 'string'", + }); + }); + }); +}); 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..8c10f3103c --- /dev/null +++ b/packages/compiler/test/checker/values/string-values.test.ts @@ -0,0 +1,119 @@ +import { strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { expectDiagnostics } from "../../../src/testing/expect.js"; +import { compileValue, diagnoseValue } from "./utils.js"; + +describe("instantiate with constructor", () => { + it("string", async () => { + const value = await compileValue(`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 compileValue(`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 compileValue(`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 compileValue(`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 diagnoseValue( + `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 compileValue(`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("interpolate another const", async () => { + 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 diagnoseValue(`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.", + }); + }); + + it("emit error if string template if interpolating non serializable value", async () => { + const diagnostics = await diagnoseValue(`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", () => { + 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 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 new file mode 100644 index 0000000000..c1c895ca66 --- /dev/null +++ b/packages/compiler/test/checker/values/utils.ts @@ -0,0 +1,116 @@ +import { ok } from "assert"; +import { Diagnostic, Model, Type, Value, definePackageFlags } 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; end?: number }> { + const runner = await createTestRunner(); + 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, end }; +} + +export async function compileAndDiagnoseValue( + 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 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 diagnoseValue(code: string, other?: string): Promise { + const [_, diagnostics] = await compileAndDiagnoseValue(code, other); + return diagnostics; +} + +export async function compileAndDiagnoseValueOrType( + constraint: string, + code: string, + other?: string +): Promise<[Type | Value | undefined, readonly Diagnostic[]]> { + const host = await createTestHost(); + host.addJsFile("collect.js", { + $collect: () => {}, + $flags: definePackageFlags({ decoratorArgMarshalling: "new" }), + }); + 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/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..7218f027d8 --- /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 { 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"; + +it("it change model expression to an object value", async () => { + await expectCodeFixOnAst( + ` + model Foo { + a: string[] = ┆{foo: "abc"}; + } + `, + (node) => { + strictEqual(node.kind, SyntaxKind.ModelExpression); + return createModelToObjectValueCodeFix(node); + } + ).toChangeTo(` + model Foo { + a: string[] = #{foo: "abc"}; + } + `); +}); 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..89bd56fcbe --- /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 { 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"; + +it("it change tuple to a array value", async () => { + await expectCodeFixOnAst( + ` + model Foo { + a: string[] = ┆["abc"]; + } + `, + (node) => { + strictEqual(node.kind, SyntaxKind.TupleExpression); + return createTupleToArrayValueCodeFix(node); + } + ).toChangeTo(` + model Foo { + a: string[] = #["abc"]; + } + `); +}); diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index a4ea14661e..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 '123' is not assignable to parameter of type 'valueof string'`, }); }); }); @@ -267,7 +266,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, }); }); @@ -320,7 +318,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, }); }); }); @@ -347,7 +344,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: `Argument '123' is not assignable to parameter of type 'valueof string'`, }); }); }); @@ -503,7 +499,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 +516,6 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '4' is not assignable to parameter of type 'valueof string'", }, ]); }); @@ -708,7 +703,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 +829,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", }); }); @@ -857,7 +852,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/range-limits.test.ts b/packages/compiler/test/decorators/range-limits.test.ts index db9cf248ec..1220135b73 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-state.js"; +import { Model } from "../../src/core/types.js"; import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.js"; describe("compiler: range limiting decorators", () => { @@ -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/compiler/test/decorators/service.test.ts b/packages/compiler/test/decorators/service.test.ts index 5b62eb4b92..02351e769c 100644 --- a/packages/compiler/test/decorators/service.test.ts +++ b/packages/compiler/test/decorators/service.test.ts @@ -65,7 +65,7 @@ describe("compiler: service", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", message: - "Argument '(anonymous model)' is not assignable to parameter of type 'ServiceOptions'", + "Argument of type '{ title: 123 }' is not assignable to parameter of type 'ServiceOptions'", }); }); @@ -103,7 +103,7 @@ describe("compiler: service", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", message: - "Argument '(anonymous model)' 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/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index c1117f9477..43b03224f8 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: ` @@ -2462,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 {} `, }); }); @@ -2473,21 +2540,10 @@ alias A = valueof string; it("keeps parentheses around valueof inside a union", async () => { await assertFormat({ code: ` -alias A = (valueof string) | Model; -`, - expected: ` -alias A = (valueof string) | Model; -`, - }); - }); - - it("keeps parentheses around valueof inside a array expression", async () => { - await assertFormat({ - code: ` -alias A = (valueof string)[]; +model Foo{} `, expected: ` -alias A = (valueof string)[]; +model Foo {} `, }); }); @@ -2807,4 +2863,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/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/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 6d78183e8a..d65cb17f0d 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: "'{' expected." }]]]); }); describe("interface statements", () => { @@ -223,12 +226,62 @@ 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("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"};`, + `const A = #{a: "abc", b: "def"};`, + `const A = #{a: "abc", ...B};`, + `const A = #{a: "abc", ...B, c: "ghi"};`, + ]); + }); + + describe("array literals", () => { + parseEach([ + `const A = #["abc"];`, + `const A = #["abc", 123];`, + `const A = #["abc", 123, #{nested: true}];`, + ]); + }); + 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 "123" = 123;`, `alias A = Foo;`]); + parseErrorEach([ + [`alias A = typeof #{}`, [{ message: "Typeof expects a value literal or value reference." }]], ]); }); 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; diff --git a/packages/compiler/test/projection/projector-identity.test.ts b/packages/compiler/test/projection/projector-identity.test.ts index 5dfaf1bb89..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 } 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,16 +376,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 (isType(arg)) { + ok(arg.projector === original.projector); + } } for (const value of original.templateMapper.map.values()) { - ok(value.projector === original.projector); + if (isType(value)) { + ok(value.projector === original.projector); + } } for (const arg of projected.templateMapper.args) { - ok(arg.projector === projected.projector); + if (isType(arg)) { + ok(arg.projector === projected.projector); + } } for (const value of projected.templateMapper.map.values()) { - ok(value.projector === projected.projector); + if (isType(value)) { + ok(value.projector === projected.projector); + } } } }); 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/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index f364c5eeae..05a1173b7b 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"), @@ -50,6 +51,8 @@ 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"), }, @@ -84,6 +87,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: { @@ -322,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"); @@ -746,6 +768,40 @@ 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("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 () => { @@ -1060,6 +1116,146 @@ 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("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("#{}"); + deepStrictEqual(tokens, [Token.punctuation.openHashBrace, Token.punctuation.closeBrace]); + }); + + it("single prop", async () => { + const tokens = await tokenizeWithConst(`#{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 tokenizeWithConst(`#{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 tokenizeWithConst(`#{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 tokenizeWithConst(`#{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("array literals", () => { + it("empty", async () => { + const tokens = await tokenizeWithConst("#[]"); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBracket, + Token.punctuation.closeBracket, + ]); + }); + + it("single value", async () => { + const tokens = await tokenizeWithConst(`#["John"]`); + deepStrictEqual(tokens, [ + Token.punctuation.openHashBracket, + Token.literals.stringQuoted("John"), + Token.punctuation.closeBracket, + ]); + }); + + it("multiple values", async () => { + const tokens = await tokenizeWithConst(`#["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 tokenizeWithConst(`#[#[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);"); @@ -1406,6 +1602,20 @@ function testColorization(description: string, tokenize: Tokenize) { }); }); }); + + 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]); + } + + return tokens.slice(common.length); + } } const punctuationMap = getPunctuationMap(); 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 () => { 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(); 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; + }, + }; +} diff --git a/packages/html-program-viewer/src/ui.tsx b/packages/html-program-viewer/src/ui.tsx index 031436ae99..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", @@ -268,6 +269,7 @@ const ScalarUI: FunctionComponent<{ type: Scalar }> = ({ type }) => { properties={{ baseScalar: "ref", derivedScalars: "ref", + constructors: "nested", }} /> ); @@ -283,6 +285,7 @@ const ModelPropertyUI: FunctionComponent<{ type: ModelProperty }> = ({ type }) = optional: "value", sourceProperty: "ref", default: "value", + defaultValue: "value", }} /> ); diff --git a/packages/http/README.md b/packages/http/README.md index a1c847b0ad..95f35ff1d6 100644 --- a/packages/http/README.md +++ b/packages/http/README.md @@ -405,7 +405,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/lib/auth.tsp b/packages/http/lib/auth.tsp index 2fdc29f843..f1ecf4eb93 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; @@ -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/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 66fcdbe982..cc4a655b3a 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -81,18 +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 '(anonymous model)' 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'", }, ]); }); @@ -170,18 +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 '(anonymous model)' 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'", }, ]); }); @@ -309,7 +297,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: `Argument '(anonymous model)' is not assignable to parameter of type '(anonymous model)'`, }, ]); }); @@ -364,7 +351,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, [ { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }, ]); }); @@ -618,7 +604,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); @@ -630,7 +615,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'valueof string'", }); }); @@ -654,7 +638,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument '123' is not assignable to parameter of type 'Record'", }); }); @@ -714,7 +697,6 @@ describe("http: decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-argument", - message: "Argument 'anOp' is not assignable to parameter of type '{} | Union | {}[]'", }); }); @@ -730,13 +712,10 @@ 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: "invalid-argument", }, { - code: "unassignable", - message: - "Type 'Flow' is not assignable to type 'TypeSpec.Http.AuthorizationCodeFlow | TypeSpec.Http.ImplicitFlow | TypeSpec.Http.PasswordFlow | TypeSpec.Http.ClientCredentialsFlow'", + code: "invalid-argument", }, ]); }); @@ -860,7 +839,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 void; /** diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 67c0a0cdd7..de5c4bdced 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, @@ -35,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; @@ -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) => { diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index eaec06d930..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,22 +34,9 @@ import { getPattern, getRelativePathFromDirectory, getSummary, - IntrinsicType, isArrayModelType, isNullType, - Model, - ModelProperty, - NumericLiteral, - Program, - Scalar, - StringLiteral, - StringTemplate, - stringTemplateToString, - Tuple, - Type, typespecTypeToJson, - Union, - UnionVariant, } from "@typespec/compiler"; import { ArrayBuilder, @@ -54,6 +54,7 @@ import { } from "@typespec/compiler/emitter-framework"; import { stringify } from "yaml"; 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> { @@ -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); } @@ -237,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/json-schema/src/lib.ts b/packages/json-schema/src/lib.ts index d6c87d1e47..5239c5e4fe 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, + definePackageFlags, + 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 = definePackageFlags({ + decoratorArgMarshalling: "new", +}); export const { reportDiagnostic, createStateSymbol } = $lib; diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index fb6ae8a92e..b27b66f8b3 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'", }); }); @@ -170,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/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 20e1355bcd..4cf5c04e4f 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), }; @@ -1301,8 +1301,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 dbcf9546ce..3413b284c1 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -17,7 +17,9 @@ import { TypeNameOptions, Union, UnionVariant, + Value, compilerAssert, + explainStringTemplateNotSerializable, getDeprecated, getDiscriminatedUnion, getDiscriminator, @@ -44,7 +46,6 @@ import { isSecret, isTemplateDeclaration, resolveEncodedName, - stringTemplateToString, } from "@typespec/compiler"; import { ArrayBuilder, @@ -366,8 +367,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)) { @@ -410,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" }; + if (string.stringValue !== undefined) { + return { type: "string", enum: [string.stringValue] }; } - return { type: "string", enum: [value] }; + const diagnostics = explainStringTemplateNotSerializable(string); + this.emitter + .getProgram() + .reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" }))); + return { type: "string" }; } numericLiteral(number: NumericLiteral): EmitterOutput { @@ -989,46 +990,24 @@ const B = { }, } as const; -export function getDefaultValue(program: Program, type: Type, defaultType: Type): 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": - 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/openapi3/test/array.test.ts b/packages/openapi3/test/array.test.ts index c31e5554d4..fd4e3ffa51 100644 --- a/packages/openapi3/test/array.test.ts +++ b/packages/openapi3/test/array.test.ts @@ -155,8 +155,42 @@ describe("openapi3: Array", () => { "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("can specify array defaults using tuple syntax (LEGACY)", async () => { + const res = await oapiForModel( + "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]; }; ` @@ -198,9 +232,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/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'", }); }); 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 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 const $reserve: ReserveDecorator = ( @@ -145,12 +127,7 @@ export const $reserve: ReserveDecorator = ( target: Type, ...reservations: readonly (unknown | number | string)[] ) => { - const finalReservations = reservations - .map((reservation) => - typeof reservation === "object" ? getTuple(ctx.program, reservation as Type) : 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/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index 1f620e1779..535d5e12c2 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, + isType, 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(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, 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(isType(valueType), "Cannot be a value type"); // Nested arrays are not supported. if (isArray(valueType)) { @@ -1036,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; diff --git a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt index 794e8483e6..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 @@ - - 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 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/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/options-invalid/diagnostics.txt b/packages/protobuf/test/scenarios/options-invalid/diagnostics.txt index 904a5d524d..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 '(anonymous model)' 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' 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; } 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; } diff --git a/packages/rest/test/routes.test.ts b/packages/rest/test/routes.test.ts index b070d47419..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 'valueof "/" | ":" | "/:"'` - ); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + }); }); it("skips templated operations", async () => { 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; } 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(); diff --git a/packages/samples/specs/string-template/main.tsp b/packages/samples/specs/string-template/main.tsp index 330845b4f7..ba422dab7f 100644 --- a/packages/samples/specs/string-template/main.tsp +++ b/packages/samples/specs/string-template/main.tsp @@ -12,12 +12,12 @@ 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 { - kind: T; + 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 diff --git a/packages/spec/src/spec.emu.html b/packages/spec/src/spec.emu.html index 46840457fc..9e4dc311c1 100644 --- a/packages/spec/src/spec.emu.html +++ b/packages/spec/src/spec.emu.html @@ -331,11 +331,22 @@

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 `;`? + +ScalarMemberList : + ScalarMember + ScalarMemberList `;` ScalarMember + +ScalarMember: + `init` Identifier `(` FunctionParameterList? `)` + ExtendsModelHeritage : `extends` Expression @@ -430,8 +441,13 @@

Syntactic Grammar

Identifier TemplateParameterConstraint? TemplateParameterDefault? TemplateParameterConstraint : - `extends` Expression - + `extends` MixedParameterConstraint + +MixedParameterConstraint : + UnionExpressionOrHigher[+InParameter] + valueof UnionExpressionOrHigher + + TemplateParameterDefault : `=` Expression @@ -458,26 +474,33 @@

Syntactic Grammar

Expression : UnionExpressionOrHigher -UnionExpressionOrHigher : - IntersectionExpressionOrHigher - `|`? UnionExpressionOrHigher `|` IntersectionExpressionOrHigher +UnionExpressionOrHigher[InParameter] : + IntersectionExpressionOrHigher[?InParameter] + `|`? UnionExpressionOrHigher[?InParameter] `|` IntersectionExpressionOrHigher[?InParameter] + +IntersectionExpressionOrHigher[InParameter] : + ArrayExpressionOrHigher[?InParameter] + `&`? IntersectionExpressionOrHigher[?InParameter] `&` ArrayExpressionOrHigher[?InParameter] -IntersectionExpressionOrHigher : - ValueOfExpressionOrHigher - `&`? IntersectionExpressionOrHigher `&` ValueOfExpressionOrHigher +ValueOfExpression : + `valueof` Expression -ValueOfExpressionOrHigher : - ArrayExpressionOrHigher - `valueof` ArrayExpressionOrHigher +TypeOfExpression : + `typeof` Literal + `typeof` ReferenceExpression + `typeof` ParenthesizedExpression -ArrayExpressionOrHigher : - PrimaryExpression - ArrayExpressionOrHigher `[` `]` +ArrayExpressionOrHigher[InParameter] : + PrimaryExpression[?InParameter] + ArrayExpressionOrHigher[?InParameter] `[` `]` -PrimaryExpression : +PrimaryExpression[InParameter] : + TypeOfExpression Literal - ReferenceExpression - ParenthesizedExpression + CallOrReferenceExpression + ParenthesizedExpression[?InParameter] + ObjectLiteral + ArrayLiteral ModelExpression TupleExpression @@ -486,7 +509,13 @@

Syntactic Grammar

BooleanLiteral NumericLiteral -ReferenceExpression : +CallOrReferenceExpression : + CallExpression + ReferenceExpression +CallExpression + IdentifierOrMemberExpression CallArguments + +ReferenceExpression IdentifierOrMemberExpression TemplateArguments? ReferenceExpressionList : @@ -503,8 +532,29 @@

Syntactic Grammar

ProjectionArguments : `(` ExpressionList? `)` -ParenthesizedExpression : - `(` Expression `)` +ParenthesizedExpression[InParameter] : + [~InParameter] `(` Expression `)` + [+InParameter] `(` MixedParameterConstraint `)` + +ObjectLiteral : + `#{` ObjectLiteralBody? `}` + +ObjectLiteralBody : + ModelPropertyList `,`? + +ObjectLiteralPropertyList : + ObjectLiteralProperty + ObjectLiteralPropertyList `,` ObjectLiteralProperty + +ObjectLiteralProperty : + ObjectLiteralSpreadProperty + Identifier `:` Expression + +ObjectLiteralSpreadProperty : + `...` ReferenceExpression + +ArrayLiteral : + `#[` ExpressionList? `]` ModelExpression : `{` ModelBody? `}` @@ -525,6 +575,10 @@

Syntactic Grammar

DecoratorArguments : `(` ExpressionList? `)` +CallExpression : + IdentifierOrMemberExpression `(` ExpressionList? `)` + + AugmentDecoratorStatement : `@@` IdentifierOrMemberExpression DecoratorArguments? @@ -542,7 +596,7 @@

Syntactic Grammar

FunctionModifiers? `fn` `(` FunctionParameterList? `)` TypeAnnotation? TypeAnnotation: - `:` Expression + `:` 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 bed1be65c8..04458b306f 100644 --- a/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/decorators-signatures.ts @@ -1,13 +1,13 @@ import { DocTag, - FunctionParameter, IntrinsicScalarName, + MixedFunctionParameter, + MixedParameterConstraint, Model, Program, Scalar, SyntaxKind, Type, - ValueType, getSourceLocation, isArrayModelType, isUnknownType, @@ -104,7 +104,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) { @@ -114,25 +114,58 @@ export function generateSignatures(program: Program, decorators: DecoratorSignat } } - function getRestTSParmeterType(type: Type | ValueType) { - if (type.kind === "Value") { - if (type.target.kind === "Model" && isArrayModelType(program, type.target)) { - return `(${getValueTSType(type.target.indexer.value)})[]`; + /** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ + function extractRestParamConstraint( + constraint: MixedParameterConstraint + ): MixedParameterConstraint | undefined { + let valueType: Type | undefined; + let type: Type | undefined; + if (constraint.valueType) { + if ( + constraint.valueType.kind === "Model" && + isArrayModelType(program, constraint.valueType) + ) { + valueType = constraint.valueType.indexer.value; } else { - return "unknown"; + return undefined; } } - 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 undefined; + } } - return `${getTSParmeterType(type.indexer.value)}[]`; + return { + entityKind: "MixedParameterConstraint", + type, + valueType, + }; + } + function getRestTSParmeterType(constraint: MixedParameterConstraint) { + const restItemConstraint = extractRestParamConstraint(constraint); + if (restItemConstraint === undefined) { + return "unknown"; + } + return `(${getTSParmeterType(restItemConstraint)})[]`; } - function getTSParmeterType(type: Type | ValueType, isTarget?: boolean): string { - if (type.kind === "Value") { - return getValueTSType(type.target); + function getTSParmeterType(constraint: MixedParameterConstraint, isTarget?: boolean): string { + if (constraint.type && constraint.valueType) { + return `${getTypeConstraintTSType(constraint.type, isTarget)} | ${getValueTSType(constraint.valueType)}`; } + if (constraint.valueType) { + return getValueTSType(constraint.valueType); + } 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"); } @@ -142,7 +175,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(" | "); @@ -190,21 +223,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 a63946a373..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 { Type, ValueType, getTypeName, resolvePath } from "@typespec/compiler"; +import { + Entity, + MixedParameterConstraint, + getEntityName, + isType, + resolvePath, +} from "@typespec/compiler"; import { readFile } from "fs/promises"; import { stringify } from "yaml"; import { @@ -187,23 +193,24 @@ export class MarkdownRenderer { return [base]; } - ref(type: Type | ValueType): string { - const namedType = type.kind !== "Value" && this.refDoc.getNamedTypeRefDoc(type); + 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 (type.kind === "Model" && type.name === "" && type.properties.size > 0) { - return inlinecode("{...}"); + if ("kind" in type && type.kind === "Model" && type.name === "" && type.properties.size > 0) { + return inlinecode(prefix + "{...}"); } return inlinecode( - getTypeName(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 +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.ref(param.type.type), param.doc]); + paramTable.push([param.name, this.MixedParameterConstraint(param.type.type), param.doc]); } content.push(section("Parameters", [table(paramTable), ""])); } else { @@ -272,6 +279,13 @@ export class MarkdownRenderer { return section(this.headingTitle(dec), content); } + MixedParameterConstraint(constraint: MixedParameterConstraint): 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; diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index 15b56f8d96..43a6b01eaa 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, @@ -13,14 +14,10 @@ import { TemplateParameterDeclarationNode, Type, UnionVariant, - ValueType, } from "@typespec/compiler"; /** @internal */ -export function getTypeSignature(type: Type | ValueType): string { - if (type.kind === "Value") { - return `valueof ${getTypeSignature(type.target)}`; - } +export function getTypeSignature(type: Type): string { if (isReflectionType(type)) { return type.name; } @@ -49,6 +46,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": @@ -120,7 +119,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) { 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..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,3 +1,4 @@ +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"; @@ -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: definePackageFlags({ + decoratorArgMarshalling: "new", + }), + }); await host.diagnose("main.tsp", { parseOptions: { comments: true, docs: true }, }); @@ -148,8 +155,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 +166,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 +214,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 +225,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; `, diff --git a/packages/versioning/src/validate.ts b/packages/versioning/src/validate.ts index d3dd6f4e59..1a1d4a1e04 100644 --- a/packages/versioning/src/validate.ts +++ b/packages/versioning/src/validate.ts @@ -3,6 +3,7 @@ import { getService, getTypeName, isTemplateInstance, + isType, 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 (isType(param)) { + validateTargetVersionCompatible(program, source, param); + } } } diff --git a/packages/versioning/test/versioned-dependencies.test.ts b/packages/versioning/test/versioned-dependencies.test.ts index 652609c00d..2497b32451 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 '[[VersionedLib.Versions.l1, VersionedLib.Versions.l1]]' is not assignable to parameter of type 'EnumMember'", }); }); }); diff --git a/packages/website/sidebars.ts b/packages/website/sidebars.ts index 7af26a6a5b..28eb23916b 100644 --- a/packages/website/sidebars.ts +++ b/packages/website/sidebars.ts @@ -95,6 +95,7 @@ const sidebars: SidebarsConfig = { "language-basics/intersections", "language-basics/type-literals", "language-basics/aliases", + "language-basics/values", "language-basics/type-relations", ], },