This commit is contained in:
Brian Terlson 2023-01-24 11:14:40 -08:00 коммит произвёл GitHub
Родитель 2f6b5c64ce
Коммит d3a2f46bf9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 3623 добавлений и 8 удалений

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/compiler",
"comment": "Feature: add an emitter framework to simplify building emitters",
"type": "none"
}
],
"packageName": "@cadl-lang/compiler"
}

78
common/config/rush/pnpm-lock.yaml сгенерированный
Просмотреть файл

@ -110,6 +110,7 @@ specifiers:
rimraf: ~3.0.2
rollup: ~3.4.0
rollup-plugin-visualizer: ~5.8.0
sinon: ~15.0.1
source-map-support: ~0.5.19
strip-json-comments: ~4.0.0
swagger-ui: ~4.15.5
@ -235,6 +236,7 @@ dependencies:
rimraf: 3.0.2
rollup: 3.4.0
rollup-plugin-visualizer: 5.8.3_rollup@3.4.0
sinon: 15.0.1
source-map-support: 0.5.21
strip-json-comments: 4.0.0
swagger-ui: 4.15.5
@ -4984,6 +4986,30 @@ packages:
engines: {node: '>=6'}
dev: false
/@sinonjs/commons/2.0.0:
resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==}
dependencies:
type-detect: 4.0.8
dev: false
/@sinonjs/fake-timers/10.0.2:
resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==}
dependencies:
'@sinonjs/commons': 2.0.0
dev: false
/@sinonjs/samsam/7.0.1:
resolution: {integrity: sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==}
dependencies:
'@sinonjs/commons': 2.0.0
lodash.get: 4.4.2
type-detect: 4.0.8
dev: false
/@sinonjs/text-encoding/0.7.2:
resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==}
dev: false
/@slorber/static-site-generator-webpack-plugin/4.0.7:
resolution: {integrity: sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==}
engines: {node: '>=14'}
@ -5449,6 +5475,16 @@ packages:
'@types/node': 18.11.9
dev: false
/@types/sinon/10.0.13:
resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==}
dependencies:
'@types/sinonjs__fake-timers': 8.1.2
dev: false
/@types/sinonjs__fake-timers/8.1.2:
resolution: {integrity: sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==}
dev: false
/@types/sockjs/0.3.33:
resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==}
dependencies:
@ -10277,6 +10313,10 @@ packages:
graceful-fs: 4.2.10
dev: false
/just-extend/4.2.1:
resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==}
dev: false
/keyborg/1.2.1:
resolution: {integrity: sha512-PXjcJb7d4ecncFnJgq1TzLBx38+LbTPDpbwNCXebMzp3xaZeG//7ydWpISouBVyjRtJFuIvpIryme4U2dYGUEg==}
dev: false
@ -10420,6 +10460,10 @@ packages:
resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==}
dev: false
/lodash.get/4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
dev: false
/lodash.memoize/4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
dev: false
@ -10902,6 +10946,16 @@ packages:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: false
/nise/5.1.4:
resolution: {integrity: sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==}
dependencies:
'@sinonjs/commons': 2.0.0
'@sinonjs/fake-timers': 10.0.2
'@sinonjs/text-encoding': 0.7.2
just-extend: 4.2.1
path-to-regexp: 1.8.0
dev: false
/no-case/3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
@ -13278,6 +13332,17 @@ packages:
simple-concat: 1.0.1
dev: false
/sinon/15.0.1:
resolution: {integrity: sha512-PZXKc08f/wcA/BMRGBze2Wmw50CWPiAH3E21EOi4B49vJ616vW4DQh4fQrqsYox2aNR/N3kCqLuB0PwwOucQrg==}
dependencies:
'@sinonjs/commons': 2.0.0
'@sinonjs/fake-timers': 10.0.2
'@sinonjs/samsam': 7.0.1
diff: 5.0.0
nise: 5.1.4
supports-color: 7.2.0
dev: false
/sirv/1.0.19:
resolution: {integrity: sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==}
engines: {node: '>= 10'}
@ -13945,6 +14010,11 @@ packages:
prelude-ls: 1.2.1
dev: false
/type-detect/4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
dev: false
/type-fest/0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
@ -15146,7 +15216,7 @@ packages:
dev: false
file:projects/compiler.tgz:
resolution: {integrity: sha512-u3LfftlK0A6nTGnQWJVWQsNbP/xY0aswwod6Al+dwogdN6KESuiJ4a9Y1+PJCo5ExSSL+z2LEPE97hyleXPRHA==, tarball: file:projects/compiler.tgz}
resolution: {integrity: sha512-5Bas9duOWtFvzc05G0QUM1EStSwVdWNnros9Pm0uzd/qSP7ED4Y7zcTd6eORO4JaWvTWSiQgGSUc4do6ZMC7QA==, tarball: file:projects/compiler.tgz}
name: '@rush-temp/compiler'
version: 0.0.0
dependencies:
@ -15159,6 +15229,7 @@ packages:
'@types/node': 18.11.9
'@types/prettier': 2.6.0
'@types/prompts': 2.4.1
'@types/sinon': 10.0.13
'@types/yargs': 17.0.13
ajv: 8.11.2
c8: 7.12.0
@ -15179,6 +15250,7 @@ packages:
prettier-plugin-organize-imports: 3.2.0_prettier@2.8.1+typescript@4.9.3
prompts: 2.4.2
rimraf: 3.0.2
sinon: 15.0.1
source-map-support: 0.5.21
typescript: 4.9.3
vscode-languageserver: 8.0.2
@ -15454,7 +15526,7 @@ packages:
dev: false
file:projects/ref-doc.tgz:
resolution: {integrity: sha512-ZZFxEHmpahIxxgTL7emDOYK6ORPoRxx3prrAo4sjef+75Gss+nVrdpXBOq7GlT7FQq4Qq3XkgHQZecOv3nV2Ug==, tarball: file:projects/ref-doc.tgz}
resolution: {integrity: sha512-DGZJoIyG/16I9kJA8diS9HwhXw4+lJdWENJETQU/2qOQVI1BTUZ327o/dPwxB5SM7hdVSU4NUMpe2C8JO3MYSg==, tarball: file:projects/ref-doc.tgz}
name: '@rush-temp/ref-doc'
version: 0.0.0
dependencies:
@ -15554,7 +15626,7 @@ packages:
dev: false
file:projects/website.tgz_@types+react@18.0.25:
resolution: {integrity: sha512-3GsmNKLHFMiEtrHwU5lN7UnZSRtZ9f3eFS2/qq9ERMRhDdTiTX6ywOb9iLyVboSSenPIQ6vw7dzrqS/Ti0eDaQ==, tarball: file:projects/website.tgz}
resolution: {integrity: sha512-v3JJXhRMQKElZdo3gNMDqnmcnPYQRq2u4f8xTqlvHnOn//tyqQhW1STh2J3SW01Af2sc/ab0ZZoAlo6ResqbjA==, tarball: file:projects/website.tgz}
id: file:projects/website.tgz
name: '@rush-temp/website'
version: 0.0.0

Просмотреть файл

@ -64,6 +64,8 @@ words:
- vswhere
- westus
- xplat
- keyer
- interner
ignorePaths:
- "**/node_modules/**"
- "**/dist/**"

Просмотреть файл

@ -0,0 +1,359 @@
---
id: emitter-framework
title: Emitter framework
---
The emitter framework makes writing emitters from Cadl to other assets a fair bit easier than manually consuming the type graph. The framework gives you an easy way to handle all the types Cadl might throw at you and know when you're "feature complete". It also handles a lot of hard problems for you, such as how to construct references between types, how to handle circular references, or how to propagate the context of the types you're emitting based on their containers or where they're referenced from. Lastly, it provides a class-based inheritance model that makes it fairly painless to extend and customize existing emitters.
## Getting Started
Make sure to read the getting started section under the [emitter basics](./emitters-basics.md) topic. To use the framework, you will need an emitter library and `$onEmit` function ready to go.
## Implementing your emitter
Implementing an emitter using the emitter framework will use a variety of types from the framework. To give you a high level overview, these are:
- `AssetEmitter`: The asset emitter is the main type you will interact with in your `$onEmit` function. You can pass the asset emitter types to emit, and tell it to write types to disk or give you source files for you to process in other ways.
- `TypeEmitter`: The type emitter is the base class for most of your emit logic. Every Cadl type has a corresponding method on TypeEmitter. It also is where you will manage your emit context, making it easy to answer such questions as "is this type inside something I care about" or "was this type referenced from something".
- `CodeTypeEmitter`: A subclass of `TypeEmitter` that makes building source code easier.
- `StringBuilder`, `ObjectBuilder`, `ArrayBuilder`: when implementing your `TypeEmitter` you will likely use these classes to help you build strings and object graphs. These classes take care of handling the placeholders that result from circular references.
Let's walk through each of these types in turn.
### `AssetEmitter<T>`
The asset emitter is responsible for driving the emit process. It has methods for taking Cadl types to emit, and maintains the state of your current emit process including the declarations you've accumulated, current emit context, and converting your emitted content into files on disk.
To create your asset emitter, call `createAssetEmitter` on your emit context in `$onEmit`. It takes the TypeEmitter which is covered in the next section. Once created, you can call `emitProgram()` to emit every type in the Cadl graph. Otherwise, you can call `emitType(someType)` to emit specific types instead.
```typescript
export async function $onEmit(context: EmitContext) {
const assetEmitter = context.createAssetEmitter(MyTypeEmitter);
// emit my entire cadl program
assetEmitter.emitProgram();
// or, maybe emit types just in a specific namespace
const ns = context.program.resolveTypeReference("MyNamespace")!;
assetEmitter.emitType(ns);
// lastly, write your emit output into the output directory
await assetEmitter.writeOutput();
}
```
### `TypeEmitter<T>`
This is the base class for writing logic to convert Cadl types into assets in your target language. Every Cadl type has at least one method on this base class, and many have multiple methods. For example, models have both `ModelDeclaration` and `ModelLiteral` methods to handle `model Pet { }` declarations and `{ anonymous: boolean }` literals respectively.
To support emitting all Cadl types, you should expect to implement all of these methods. But if you don't want to support emitting all Cadl types, you can either throw or just not implement the method, in which case the type will not be emitted.
The generic type parameter `T` is the type of emit output you are building. For example, if you're emitting source code, `T` will be `string`. If you're building an object graph like JSON, `T` will be `object`. If your `T` is `string`, i.e. you are building source code, you will probably want to use the `CodeTypeEmitter` subclass which is a bit more convenient, but `TypeEmitter<string>` will also work fine.
A simple emitter that doesn't do much yet might look like:
```typescript
class MyCodeEmitter extends CodeTypeEmitter {
modelDeclaration(model: Model, name: string) {
console.log("Emitting a model named", name);
}
}
```
Passing this to `createAssetEmitter` and calling `assetEmitter.emitProgram()` will console.log all the models in the program.
#### EmitterOutput
Most methods of the `TypeEmitter` must either return `T` or an `EmitterOutput<T>`. There are four kinds of `EmitterOutput`:
- `Declaration<T>`: A declaration, which has a name and is declared in a scope, and so can be referenced by other emitted types (more on References later). Declarations are created by calling `this.emitter.result.declaration(name, value)` in your emitter methods. Scopes come from your current context, which is covered later in this document.
- `RawCode<T>`: Output that is in some way concatenated into the output but cannot be referenced (e.g. things like type literals). Raw code is created by calling `this.emitter.result.rawCode(value)` in your emitter methods.
- `NoEmit`: The type does not contribute any output. This is created by calling `this.emitter.result.none()` in your emitter methods.
- `CircularEmit`: Indicates that a circular reference was encountered, which is generally handled by the framework with Placeholders (see the next section). You do not need to create this result yourself, the framework will produce this when required.
When an emitter returns `T` or a `Placeholder<T>`, it behaves as if it returned `RawCode<T>` with that value.
To create these results, you use the `result.*()` APIs on `AssetEmitter`, which can be accessed via `this.emitter.result` in your methods.
With this in mind, we can make `MyCodeEmitter` a bit more functional:
```typescript
class MyCodeEmitter extends CodeTypeEmitter {
// context and scope are covered later in this document
programContext(program: Program): Context {
const sourceFile = this.emitter.createSourceFile("test.txt");
return {
scope: sourceFile.globalScope,
};
}
modelDeclaration(model: Model, name: string) {
const props = this.emitter.emitModelProperties(model);
return this.emitter.result.declaration(name, `declaration of ${name}`);
}
}
```
If we have a cadl program that looks like:
```cadl
model Pet {}
```
and we call `assetEmitter.writeOutput()`, we'll find `test.txt` contains the contents `"declaration of Pet"`.
In order to emit properties of `Pet`, we'll need to concatenate the properties of pets with the declaration we made and leverage builders to make that easy. These topics are covered in the next two sections.
#### Concatenating results
It is very rare that you only want to emit a declaration and nothing else. Probably your declaration will have various parts to it, and those parts will depend on the emit output of the parts of the type your emitting. For example, a declaration from a Cadl model will likely include members based on the members declared in the Cadl.
This is accomplished by calling `emit` or other `emit*` methods on the asset emitter from inside your `AssetEmitter` methods. For example, to emit the properties of a model declaration, we can call `this.emitter.emitModelProperties(model)`. This will invoke your the corresponding `AssetEmitter` method and return you the `EmitterOutput` result.
It is unlikely that you want to concatenate this result directly. For declarations and raw code, the `value` property is likely what you're interested in, but there are other complexities as well. So in order to concatenate results together, you probably want to use a builder.
#### Builders
Builders are helpers that make it easy to concatenate output into your final emitted asset. They do two things of note: they handle extracting the value from `Declaration` and `rawCode` output, and they handle `Placeholder` values that crop up due to circular references. Three `builders` are provided:
- Strings: Using the `code` template literal tag, you can concatenate `EmitterOutput`s together into a final string.
- Object: Constructing an `ObjectBuilder` with an object will replace any `EmitterOutput` in the object with its value and handle placeholders as necessary.
- Array: Constructing an `ArrayBuilder` will let you push `EmitterOutput` and pull out the value and placeholders as necessary.
Now with these tools, we can make `MyCodeEmitter` even more functional:
```typescript
class MyCodeEmitter extends CodeTypeEmitter {
// context is covered later in this document
programContext(program: Program): Context {
const sourceFile = this.emitter.createSourceFile("test.txt");
return {
scope: sourceFile.globalScope,
};
}
modelDeclaration(model: Model, name: string) {
const props = this.emitter.emitModelProperties(model);
return this.emitter.result.declaration(name, code`declaration of ${name} with ${props}`);
}
modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
return code`a property named ${property.name} and a type of ${this.emitter.emitType(
property.type
)}`;
}
modelLiteral(model: Model) {
return `an object literal`;
}
}
```
Now given a cadl program like:
```cadl
model Pet {
position: {};
}
```
we will find `test.txt` contains the output
> declaration of Pet with a property named position and a type of an object literal
#### References between emitted types
A common scenario when emitting to most targets is handling how to make references between types. This can get pretty complex, especially when the declarations are emitted into different scopes. The emitter framework does a lot of heavy lifting for you by calculating the scopes between your current context and the declaration you're trying to reference.
How declarations arrive in different scopes is covered in the Context section later in this document.
Let's look at the `reference` signature on the TypeEmitter:
```typescript
reference(
targetDeclaration: Declaration<string>,
pathUp: Scope<string>[],
pathDown: Scope<string>[],
commonScope: Scope<string> | null
): string | EmitEntity<string> {}
```
The `reference` function is called with:
- `targetDeclaration`: The declaration we're making a reference to.
- `pathUp`: The scopes between our current scope and the common scope.
- `pathDown`: The scopes between the common scope and the declaration we're referencing.
- `commonScope`: The nearest scope shared between our current scope and the target declaration.
So let's imagine we have declarations under the following scopes:
```
source file
namespace A
namespace B
model M1
namespace C
model M2
```
If M1 references M2, `reference` will be called with the following arguments:
- `targetDeclaration`: M2
- `pathUp`: [namespace B, namespace A]
- `pathDown`: [namespace C]
- `commonScope`: source file
For languages which walk up a scope chain in order to find a reference (e.g. TypeScript, C#, Java, etc.), you generally won't need `pathUp`, you can just join the scopes in the `pathDown` resulting in a reference like `C.M2`. Other times you may need to construct a more path-like reference, in which case you can emit for example a `../` for every item in `pathUp`, resulting in a reference like `../../C/M2`.
When the declarations don't share any scope, `commonScope` will be null. This happens when the types are contained in different source files. In such cases, your emitter will likely need to import the target declaration's source file in addition to constructing a reference. The source file has an `imports` property that can hold a list of such imports.
We can update our example emitter to generate references by adding an appropriate `references` method:
```typescript
class MyCodeEmitter extends CodeTypeEmitter {
// snip out the methods we implemented previously
// If the model is Person, put it into a special namespace.
// We will return to this in detail in the next section.
modelDeclarationContext(model: Model, name: string): Context {
if (model.name === "Person") {
const parentScope = this.emitter.getContext().scope;
const scope = this.emitter.createScope({}, "Namespace", parentScope);
return {
scope,
};
} else {
return {};
}
}
reference(
targetDeclaration: Declaration<string>,
pathUp: Scope<string>[],
pathDown: Scope<string>[],
commonScope: Scope<string> | null
): string | EmitEntity<string> {
const segments = pathDown.map((s) => s.name);
segments.push(targetDeclaration.name);
return `a reference to ${segments.join(".")}`;
}
}
```
Now if we emit the following Cadl program:
```cadl
model Pet {
person: Person;
}
model Person {
pet: Pet;
}
```
We will find that `test.txt` contains the following text:
> declaration of Pet with a property named person and a type of a reference to Namespace.Person
#### Placeholders
Consider the following Cadl program:
```cadl
model Pet {
owner: Person;
}
model Person {
pet: Pet;
}
```
In order to emit `Pet`, we need to emit `Person`, so we go to emit that. But in order to emit `Person`, we need to emit `Pet`, which is what we're already trying to do! We're at an impasse. This is a circular reference.
The emitter framework handles circular references via `Placeholder`s. When a circular reference is encountered, the `value` of an `EmitterOutput` is set to a placeholder that is filled in when we've finished constructing the thing we referenced. So in the case above, when emitting `Person` and we come across the circular reference to `Pet`, we'll return a `Placeholder`. We'll then come back to `Pet`, finish it and return an `EmitterOutput` for it, and then set any `Placeholder`s waiting for `Pet` to that output.
If you're using the `Builder`s that come with the framework, you will not need to worry about dealing with `Placeholder` yourself.
#### Context
A common need when emitting Cadl is to know what context you are emitting the type in. There is one piece of required context: `scope`, which tells the emitter framework where you want to place your declarations. But you might also want to easily answer questions like: am I emitting a model inside a particular namespace? Or am I emitting a model that is referenced from the return type of an operation? The emitter framework makes managing this context fairly trivial.
Every method that results in an `EmitterOutput` has a corresponding method for setting lexical and reference context. We saw this above when we created `modelDeclarationContext` in order to put some models into a different namespace.
##### Lexical Context
Lexical context is available when emitting types that are lexically contained within the emitted entity in the source Cadl. For example, if we set `modelDeclarationContext`, that context will be visible when emitting the model's properties and any nested model literals.
##### Reference Context
Reference context is passed along when making references and otherwise propagates lexically. For example, if we set `modelDeclarationReferenceContext`, that context will be visible when emitting the model's properties and any nested model literals just like with lexical context. But unlike with lexical context, if the current model references another type, then the reference context will be visible when emitting the referenced model.
Note that this means that we may emit the same model multiple times. Consider the following Cadl program:
```cadl
model Pet {}
model Person {
pet: Pet;
}
```
If, when emitting Person, we set the reference context to `{ refByPerson: true }`, we will call `emitModel` for `Pet` twice, once with no context set, and once again with the context we set when emitting `Person`. This behavior is very handy when you want to emit the same model different ways depending on how it is used, e.g. when your emit differs whether a model is an input type or output type, or when a model's properties differ based on any `@visibility` decorators and the context the model appears in (e.g. for Resources, whether it's being read, updated, created, deleted, etc.).
#### Scope
The scope that declarations are created in is set in using context. When emitting all of your Cadl program into the same file, and not emitting types into any kind of namespace, it suffices to set scope once in `programContext`. Call `this.emitter.createSourceFile("filePath.txt")` to create a source file, which comes with a scope ready to use.
To emit into different source files, e.g. if we want to emit using a "one class per file" pattern, move the into a more granular context method. For example, if we instead create source files in `modelDeclarationContext`, then declarations for each model will be in their own file.
If we want to emit into namespaces under a source file, we can create scopes manually. Call `this.emitter.createScope(objectReference, name, parentScope)`. The `objectReference` is an object with metadata about the scope. You might use this to emit e.g. a namespace declaration in your target language, but often it can just be an empty object (`{}`). Name is the name of the scope, used when constructing references. And parent scope is the scope this is found under.
Lets return to our previous example:
```typescript
modelDeclarationContext(model: Model, name: string): Context {
if (model.name === "Person") {
const parentScope = this.emitter.getContext().scope;
const scope = this.emitter.createScope({}, "Namespace", parentScope);
return {
scope,
};
} else {
return {};
}
}
```
We can now see how this results in the `Person` model being located in a nested scope - because we set `scope` on the context to a new scope we created via `this.emitter.setScope`.
### Extending `TypeEmitter`
TypeEmitters are classes and explicitly support subclassing, so you can customize an existing emitter by extending it and overriding any methods you want to customize in your subclass. In fact, emitters you find out in the ecosystem are likely not to work without creating a subclass, because they only know how to emit types, but you need to provide the scope for any declarations it wants to create. For example, if we have a base `TypeScriptEmitter` that can convert Cadl into TypeScript, we might extend it to tell it to put all declarations in the same file:
```typescript
class MyTsEmitter extends TypeScriptEmitter {
programContext(program: Program): Context {
const sourceFile = this.emitter.createSourceFile("test.txt");
return {
scope: sourceFile.globalScope,
};
}
}
```
Or, if we want one class or interface per file, we might instead do something like:
```typescript
class MyTsEmitter extends TypeScriptEmitter {
modelDeclarationContext(program: Program): Context {
const sourceFile = this.emitter.createSourceFile("test.txt");
return {
scope: sourceFile.globalScope,
};
}
// and similar for other declarations: Unions, Enums, Interfaces, and Operations.
}
```

Просмотреть файл

@ -83,9 +83,17 @@ Generally speaking, emitter options and decorators can solve the same problems:
The general guideline is to use a decorator when the customization is intrinsic to the API itself. In other words, when all uses of the Cadl program would use the same configuration. This is not the case for `outputFilename` because different users of the API might want to emit the files in different locations depending on how their code generation pipeline is set up.
## Querying the program
## Emitting Cadl types to assets on disk
One of the main tasks of an emitter is finding types to emit. There are two main approaches: using the Semantic Walker, which lets you easily run code for every type in the program, and doing a custom traversal, which gives you a lot more flexibility.
One of the main tasks of an emitter is finding types to emit. There are three main approaches:
1. The [emitter framework](./emitter-framework.md), which makes it relatively easy to emit all your Cadl types (or a subset, if you wish).
1. The Semantic Walker, which lets you easily run code for every type in the program
1. Custom traversal, which gives you a lot more flexibility than either of the previous approaches at the cost of some complexity.
### Emitter Framework
The emitter framework provides handles a lot of hard problems for you while providing an easy-to-use API to convert your Cadl into source code or other object graphs. Visit the [emitter framework](./emitter-framework.md) page to learn more.
### Semantic Walker
@ -149,7 +157,7 @@ function emitModel(model: Model) {
Sometimes you might want to get access to a known Cadl type in the type graph, for example a model that you have defined in your library.
An helper is provided on the program to do that.
A helper is provided on the program to do that.
```ts
program.resolveTypeReference(reference: string): Type | undefined;

Просмотреть файл

@ -1,4 +1,5 @@
import { EmitterOptions } from "../config/types.js";
import { createAssetEmitter } from "../emitter-framework/asset-emitter.js";
import { createBinder } from "./binder.js";
import { Checker, createChecker } from "./checker.js";
import { compilerAssert, createSourceFile } from "./diagnostics.js";
@ -756,6 +757,9 @@ export async function compile(
program,
emitterOutputDir: emitter.emitterOutputDir,
options: emitter.options,
getAssetEmitter(TypeEmitterClass) {
return createAssetEmitter(program, TypeEmitterClass);
},
};
try {
await emitter.emitFunction(context);

Просмотреть файл

@ -1,4 +1,6 @@
import type { JSONSchemaType as AjvJSONSchemaType } from "ajv";
import { TypeEmitter } from "../emitter-framework/index.js";
import { AssetEmitter } from "../emitter-framework/types.js";
import { Program } from "./program.js";
// prettier-ignore
@ -1781,6 +1783,13 @@ export interface EmitContext<TOptions extends object = Record<string, never>> {
* Emitter custom options defined in createCadlLibrary
*/
options: TOptions;
/**
* Get an asset emitter to write emitted output to disk using a TypeEmitter
*
* @param TypeEmitterClass The TypeEmitter to construct your emitted output
*/
getAssetEmitter<T>(TypeEmitterClass: typeof TypeEmitter<T>): AssetEmitter<T>;
}
export type LogLevel = "trace" | "warning" | "error";

Просмотреть файл

@ -0,0 +1,737 @@
import {
compilerAssert,
IntrinsicType,
isTemplateDeclaration,
Namespace,
Program,
Type,
} from "../core/index.js";
import { CustomKeyMap } from "./custom-key-map.js";
import { Placeholder } from "./placeholder.js";
import { TypeEmitter } from "./type-emitter.js";
import {
AssetEmitter,
CadlDeclaration,
CircularEmit,
ContextState,
Declaration,
EmitEntity,
EmitterResult,
EmitterState,
NamespaceScope,
NoEmit,
RawCode,
Scope,
SourceFile,
SourceFileScope,
} from "./types.js";
type EndingWith<Names, Name extends string> = Names extends `${infer _X}${Name}` ? Names : never;
export function createAssetEmitter<T>(
program: Program,
TypeEmitterClass: typeof TypeEmitter<T>,
options: Record<string, unknown> = {}
): AssetEmitter<T> {
const sourceFiles: SourceFile<T>[] = [];
const typeId = CustomKeyMap.objectKeyer();
const contextId = CustomKeyMap.objectKeyer();
// This is effectively a seen set, ensuring that we don't emit the same
// type with the same context twice. So the map stores a triple of:
//
// 1. the method of TypeEmitter we would call
// 2. the tsp type we're emitting.
// 3. the current context.
//
// Note that in order for this to work, context needs to be interned so
// contexts with the same values inside are treated as identical in the
// map. See createInterner for more details.
const typeToEmitEntity = new CustomKeyMap<[string, Type, ContextState], EmitEntity<T>>(
([method, type, context]) => {
return `${method}-${typeId.getKey(type)}-${contextId.getKey(context)}`;
}
);
// When we encounter a circular reference, this map will hold a callback
// that should be called when the circularly referenced type has completed
// its emit.
const waitingCircularRefs = new CustomKeyMap<
[string, Type, ContextState],
{
state: EmitterState;
cb: (entity: EmitEntity<T>) => EmitEntity<T>;
}[]
>(([method, type, context]) => {
return `${method}-${typeId.getKey(type)}-${contextId.getKey(context)}`;
});
// Similar to `typeToEmitEntity`, this ensures we don't recompute context
// for types that we already have context for. Note that context is
// dependent on the context of the context call, e.g. if a model is
// referenced with reference context set we need to get its declaration
// context again. So we use the context's context as a key. Context must
// be interned, see createInterner for more details.
const knownContexts = new CustomKeyMap<[Type, ContextState], ContextState>(([type, context]) => {
return `${typeId.getKey(type)}-${contextId.getKey(context)}`;
});
// The stack of types that the currently emitted type is lexically
// contained in. This gets pushed to when we visit a type that is
// lexically contained in the current type, and is reset when we jump via
// reference to another type in a different lexical context. Note that
// this does not correspond to tsp's lexical nesting, e.g. in the case of
// an alias to a model expression, the alias is lexically outside the
// model, but in the type graph we will consider it to be lexically inside
// whatever references the alias.
let lexicalTypeStack: Type[] = [];
// Internally, context is is split between lexicalContext and
// referenceContext because when a reference is made, we carry over
// referenceContext but leave lexical context behind. When context is
// accessed by the user, they are merged by getContext().
let context: ContextState = {
lexicalContext: {},
referenceContext: {},
};
let programContext: ContextState | null = null;
let incomingReferenceContext: Record<string, string> | null = null;
const interner = createInterner();
const assetEmitter: AssetEmitter<T> = {
getContext() {
return {
...context.lexicalContext,
...context.referenceContext,
};
},
getOptions() {
return options;
},
getProgram() {
return program;
},
result: {
declaration(name, value) {
const scope = currentScope();
compilerAssert(
scope,
"Emit context must have a scope set in order to create declarations. Consider setting scope to a new source file's global scope in the `programContext` method of `TypeEmitter`."
);
return new Declaration(name, scope, value);
},
rawCode(value) {
return new RawCode(value);
},
none() {
return new NoEmit();
},
},
createScope(block, name, parentScope: Scope<T> | null = null) {
let newScope: Scope<T>;
if (!parentScope) {
// create source file scope
newScope = {
kind: "sourceFile",
name,
sourceFile: block,
parentScope,
childScopes: [],
declarations: [],
} as SourceFileScope<T>;
} else {
newScope = {
kind: "namespace",
name,
namespace: block,
childScopes: [],
declarations: [],
parentScope,
} as NamespaceScope<T>;
}
parentScope?.childScopes.push(newScope);
return newScope as any; // the overload of createScope causes type weirdness
},
createSourceFile(path): SourceFile<T> {
const sourceFile = {
globalScope: undefined as any,
path,
imports: new Map(),
};
sourceFile.globalScope = this.createScope(sourceFile, "");
sourceFiles.push(sourceFile);
return sourceFile;
},
emitTypeReference(target): EmitEntity<T> {
if (target.kind === "ModelProperty") {
return invokeTypeEmitter("modelPropertyReference", target);
}
incomingReferenceContext = context.referenceContext ?? null;
const entity = this.emitType(target);
let placeholder: Placeholder<T> | null = null;
if (entity.kind === "circular") {
let waiting = waitingCircularRefs.get(entity.emitEntityKey);
if (!waiting) {
waiting = [];
waitingCircularRefs.set(entity.emitEntityKey, waiting);
}
waiting.push({
state: {
lexicalTypeStack,
context,
},
cb: (entity) => invokeReference(this, entity),
});
placeholder = new Placeholder();
return this.result.rawCode(placeholder);
} else {
return invokeReference(this, entity);
}
function invokeReference(
assetEmitter: AssetEmitter<T>,
entity: EmitEntity<T>
): EmitEntity<T> {
if (entity.kind !== "declaration") {
return entity;
}
const scope = currentScope();
compilerAssert(
scope,
"Emit context must have a scope set in order to create references to declarations."
);
const targetScope = entity.scope;
const targetChain = scopeChain(targetScope);
const currentChain = scopeChain(scope);
let diffStart = 0;
while (
targetChain[diffStart] &&
currentChain[diffStart] &&
targetChain[diffStart] === currentChain[diffStart]
) {
diffStart++;
}
const pathUp: Scope<T>[] = currentChain.slice(diffStart);
const pathDown: Scope<T>[] = targetChain.slice(diffStart);
let ref = typeEmitter.reference(
entity,
pathUp,
pathDown,
targetChain[diffStart - 1] ?? null
);
if (!(ref instanceof EmitterResult)) {
ref = assetEmitter.result.rawCode(ref) as RawCode<T>;
}
if (placeholder) {
// this should never happen as this function shouldn't be called until
// the target declaration is finished being emitted.
compilerAssert(ref.kind !== "circular", "TypeEmitter `reference` returned circular emit");
// this could presumably be allowed if we want.
compilerAssert(
ref.kind === "none" || !(ref.value instanceof Placeholder),
"TypeEmitter's `reference` method cannot return a placeholder."
);
switch (ref.kind) {
case "code":
case "declaration":
placeholder.setValue(ref.value as T);
break;
case "none":
// this cast is incorrect, think about what should happen
// if reference returns noEmit...
placeholder.setValue("" as T);
break;
}
}
return ref;
}
function scopeChain(scope: Scope<T> | null) {
const chain = [];
while (scope) {
chain.unshift(scope);
scope = scope.parentScope;
}
return chain;
}
},
emitDeclarationName(type): string {
return typeEmitter.declarationName!(type);
},
async writeOutput() {
for (const file of sourceFiles) {
const outputFile = typeEmitter.sourceFile(file);
await program.host.writeFile(outputFile.path, outputFile.contents);
}
},
emitType(type) {
const key = typeEmitterKey(type);
let args: any[];
switch (key) {
case "scalarDeclaration":
case "modelDeclaration":
case "modelInstantiation":
case "operationDeclaration":
case "interfaceDeclaration":
case "interfaceOperationDeclaration":
case "enumDeclaration":
case "unionDeclaration":
case "unionInstantiation":
const declarationName = typeEmitter.declarationName(type as CadlDeclaration);
args = [declarationName];
break;
case "intrinsic":
args = [(type as IntrinsicType).name];
break;
default:
args = [];
}
const result = (invokeTypeEmitter as any)(key, type, ...args);
return result;
},
emitProgram(options) {
const namespace = program.getGlobalNamespaceType();
if (options?.emitGlobalNamespace) {
this.emitType(namespace);
return;
}
for (const ns of namespace.namespaces.values()) {
if (ns.name === "Cadl" && !options?.emitCadlNamespace) continue;
this.emitType(ns);
}
for (const model of namespace.models.values()) {
if (!isTemplateDeclaration(model)) {
this.emitType(model);
}
}
for (const operation of namespace.operations.values()) {
if (!isTemplateDeclaration(operation)) {
this.emitType(operation);
}
}
for (const enumeration of namespace.enums.values()) {
this.emitType(enumeration);
}
for (const union of namespace.unions.values()) {
if (!isTemplateDeclaration(union)) {
this.emitType(union);
}
}
for (const iface of namespace.interfaces.values()) {
if (!isTemplateDeclaration(iface)) {
this.emitType(iface);
}
}
},
emitModelProperties(model) {
const res = typeEmitter.modelProperties(model);
if (res instanceof EmitterResult) {
return res;
} else {
return this.result.rawCode(res);
}
},
emitModelProperty(property) {
return invokeTypeEmitter("modelPropertyLiteral", property);
},
emitOperationParameters(operation) {
return invokeTypeEmitter("operationParameters", operation, operation.parameters);
},
emitOperationReturnType(operation) {
return invokeTypeEmitter("operationReturnType", operation, operation.returnType);
},
emitInterfaceOperations(iface) {
return invokeTypeEmitter("interfaceDeclarationOperations", iface);
},
emitInterfaceOperation(operation) {
const name = typeEmitter.declarationName(operation);
return invokeTypeEmitter("interfaceOperationDeclaration", operation, name);
},
emitEnumMembers(en) {
return invokeTypeEmitter("enumMembers", en);
},
emitUnionVariants(union) {
return invokeTypeEmitter("unionVariants", union);
},
emitTupleLiteralValues(tuple) {
return invokeTypeEmitter("tupleLiteralValues", tuple);
},
};
const typeEmitter = new TypeEmitterClass(assetEmitter);
return assetEmitter;
/**
* This function takes care of calling a method on the TypeEmitter to
* convert it to some emitted output. It will return a cached type if we
* have seen it before (and the context is the same). It will establish
* the emit context by calling the appropriate methods before getting the
* emit result. Also if a type emitter returns just a T or a
* Placeholder<T>, it will convert that to a RawCode result.
*/
function invokeTypeEmitter<
TMethod extends keyof Omit<
TypeEmitter<T>,
| "sourceFile"
| "declarationName"
| "reference"
| "emitValue"
| EndingWith<keyof TypeEmitter<T>, "Context">
>
>(method: TMethod, ...args: Parameters<TypeEmitter<T>[TMethod]>): EmitEntity<T> {
const type = args[0];
let entity: EmitEntity<T>;
let emitEntityKey: [string, Type, ContextState];
let cached = false;
withTypeContext(type, () => {
emitEntityKey = [method, type, context];
const seenEmitEntity = typeToEmitEntity.get(emitEntityKey);
if (seenEmitEntity) {
entity = seenEmitEntity;
cached = true;
return;
}
typeToEmitEntity.set(emitEntityKey, new CircularEmit(emitEntityKey));
compilerAssert(typeEmitter[method], `TypeEmitter doesn't have a method named ${method}.`);
entity = liftToRawCode((typeEmitter[method] as any)(...args));
});
if (cached) {
return entity!;
}
if (entity! instanceof Placeholder) {
entity.onValue((v) => handleCompletedEntity(v));
return entity;
}
handleCompletedEntity(entity!);
return entity!;
function handleCompletedEntity(entity: EmitEntity<T>) {
typeToEmitEntity.set(emitEntityKey!, entity!);
const waitingRefCbs = waitingCircularRefs.get(emitEntityKey!);
if (waitingRefCbs) {
for (const record of waitingRefCbs) {
withContext(record.state, () => {
record.cb(entity);
});
}
waitingCircularRefs.set(emitEntityKey!, []);
}
if (entity!.kind === "declaration") {
entity!.scope.declarations.push(entity!);
}
}
function liftToRawCode(value: EmitEntity<T> | Placeholder<T> | T): EmitEntity<T> {
if (value instanceof EmitterResult) {
return value;
}
return assetEmitter.result.rawCode(value);
}
}
/**
* This helper takes a type and sets the `context` state to what it should
* be in order to invoke the type emitter method for that type. This needs
* to take into account the current context and any incoming reference
* context.
*/
function setContextForType(type: Type) {
let newTypeStack;
// if we've walked into a new declaration, reset the lexical type stack
// to the lexical containers of the current type.
if (isDeclaration(type)) {
newTypeStack = [type];
let ns = type.namespace;
while (ns) {
if (ns.name === "") break;
newTypeStack.unshift(ns);
ns = ns.namespace;
}
} else {
newTypeStack = [...lexicalTypeStack, type];
}
lexicalTypeStack = newTypeStack;
if (!programContext) {
programContext = interner.intern({
lexicalContext: typeEmitter.programContext(program),
referenceContext: {},
});
}
// Establish our context by starting from program and walking up the type stack
// and merging in context for each of the lexical containers.
context = programContext;
for (const contextChainEntry of lexicalTypeStack) {
// when we're at the top of the lexical context stack (i.e. we are back
// to the type we passed in), bring in any incoming reference context.
if (contextChainEntry === type && incomingReferenceContext) {
context = interner.intern({
lexicalContext: context.lexicalContext,
referenceContext: interner.intern({
...context.referenceContext,
...incomingReferenceContext,
}),
});
incomingReferenceContext = null;
}
const seenContext = knownContexts.get([contextChainEntry, context]);
if (seenContext) {
context = seenContext;
continue;
}
// invoke the context methods
const key = typeEmitterKey(contextChainEntry);
const lexicalKey = key + "Context";
const referenceKey = typeEmitterKey(contextChainEntry) + "ReferenceContext";
compilerAssert(
(typeEmitter as any)[lexicalKey],
`TypeEmitter doesn't have a method named ${lexicalKey}`
);
const newContext = (typeEmitter as any)[lexicalKey](contextChainEntry);
const newReferenceContext = keyHasReferenceContext(key)
? (typeEmitter as any)[referenceKey](contextChainEntry)
: {};
// assemble our new reference and lexical contexts.
const newContextState = interner.intern({
lexicalContext: interner.intern({
...context.lexicalContext,
...newContext,
}),
referenceContext: interner.intern({
...context.referenceContext,
...newReferenceContext,
}),
});
knownContexts.set([contextChainEntry, context], newContextState);
context = newContextState;
}
}
/**
* Invoke the callback with the proper context for a given type.
*/
function withTypeContext(type: Type, cb: () => void) {
const oldContext = context;
const oldTypeStack = lexicalTypeStack;
setContextForType(type);
cb();
context = oldContext;
lexicalTypeStack = oldTypeStack;
}
/**
* Invoke the callback with the given context.
*/
function withContext(newContext: EmitterState, cb: () => void) {
const oldContext = newContext.context;
const oldTypeStack = newContext.lexicalTypeStack;
context = newContext.context;
lexicalTypeStack = newContext.lexicalTypeStack;
cb();
context = oldContext;
lexicalTypeStack = oldTypeStack;
}
function typeEmitterKey(type: Type) {
switch (type.kind) {
case "Model":
if (type.name === "" || type.name === "Array") {
return "modelLiteral";
}
if (type.templateMapper) {
return "modelInstantiation";
}
return "modelDeclaration";
case "Namespace":
return "namespace";
case "ModelProperty":
return "modelPropertyLiteral";
case "Boolean":
return "booleanLiteral";
case "String":
return "stringLiteral";
case "Number":
return "numericLiteral";
case "Operation":
if (type.interface) {
return "interfaceOperationDeclaration";
} else {
return "operationDeclaration";
}
case "Interface":
return "interfaceDeclaration";
case "Enum":
return "enumDeclaration";
case "EnumMember":
return "enumMember";
case "Union":
if (!type.name) {
return "unionLiteral";
}
if (type.templateMapper) {
return "unionInstantiation";
}
return "unionDeclaration";
case "UnionVariant":
return "unionVariant";
case "Tuple":
return "tupleLiteral";
case "Scalar":
return "scalarDeclaration";
case "Intrinsic":
return "intrinsic";
default:
compilerAssert(false, `Encountered type ${type.kind} which we don't know how to emit.`);
}
}
function currentScope() {
return context.referenceContext?.scope ?? context.lexicalContext?.scope ?? null;
}
}
function isDeclaration(type: Type): type is CadlDeclaration | Namespace {
switch (type.kind) {
case "Namespace":
case "Interface":
case "Enum":
case "Operation":
case "Scalar":
return true;
case "Model":
return type.name ? type.name !== "" && type.name !== "Array" : false;
case "Union":
return type.name ? type.name !== "" : false;
default:
return false;
}
}
/**
* An interner takes an object and returns either that same object, or a
* previously seen object that has the identical shape.
*
* This implementation is EXTREMELY non-optimal (O(n*m) where n = number of unique
* state objects and m = the number of properties a state object contains). This
* will very quickly be a bottleneck. That said, the common case is no state at
* all, and also this is essentially implementing records and tuples, so could
* probably adopt those when they are released. That that said, the records and
* tuples are presently facing headwinds due to implementations facing exactly
* these performance characteristics. Regardless, there are optimizations we
* could consider.
*/
function createInterner() {
const emptyObject = {};
const knownObjects: Set<Record<string, any>> = new Set();
return {
intern<T extends Record<string, any>>(object: T): T {
const keyLen = Object.keys(object).length;
if (keyLen === 0) return emptyObject as any;
for (const ko of knownObjects) {
const entries = Object.entries(ko);
if (entries.length !== keyLen) continue;
let found = true;
for (const [key, value] of entries) {
if (object[key] !== value) {
found = false;
break;
}
}
if (found) {
return ko as any;
}
}
knownObjects.add(object);
return object;
},
};
}
const noReferenceContext = new Set<string>([
"booleanLiteral",
"stringLiteral",
"numericLiteral",
"scalarDeclaration",
"enumDeclaration",
"enumMember",
"intrinsic",
]);
function keyHasReferenceContext(key: keyof TypeEmitter<any>): boolean {
return !noReferenceContext.has(key);
}

Просмотреть файл

@ -0,0 +1,38 @@
import { compilerAssert } from "../../core/index.js";
import { Placeholder } from "../placeholder.js";
import { EmitEntity, EmitterResult } from "../types.js";
export class ArrayBuilder<T> extends Array {
#setPlaceholderValue(p: Placeholder<T>, value: T) {
for (const [i, item] of this.entries()) {
if (item === p) {
this[i] = value;
}
}
}
push(...values: (EmitEntity<T> | Placeholder<T> | T)[]): number {
for (const v of values) {
let toPush: Placeholder<T> | T | null;
if (v instanceof EmitterResult) {
compilerAssert(v.kind !== "circular", "Can't push a circular emit result.");
if (v.kind === "none") {
toPush = null;
} else {
toPush = v.value;
}
} else {
toPush = v;
}
if (toPush instanceof Placeholder) {
toPush.onValue((v) => this.#setPlaceholderValue(toPush as Placeholder<T>, v));
}
super.push(toPush);
}
return values.length;
}
}

Просмотреть файл

@ -0,0 +1,35 @@
import { compilerAssert } from "../../core/index.js";
import { Placeholder } from "../placeholder.js";
import { EmitEntity, EmitterResult } from "../types.js";
// eslint is confused by merging generic interface and classes
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface ObjectBuilder<T> extends Record<string, any> {}
export class ObjectBuilder<T> {
constructor(initializer: Record<string, unknown> = {}) {
for (const [key, value] of Object.entries(initializer)) {
this.set(key, value as any);
}
}
set(key: string, v: EmitEntity<T> | Placeholder<T> | T) {
let value = v;
if (v instanceof EmitterResult) {
compilerAssert(v.kind !== "circular", "Can't set a circular emit result.");
if (v.kind === "none") {
this[key] = null;
return;
} else {
value = v.value;
}
}
if (value instanceof Placeholder) {
value.onValue((v) => {
this[key] = v;
});
}
this[key] = value;
}
}

Просмотреть файл

@ -0,0 +1,102 @@
import { Placeholder } from "../placeholder.js";
import { EmitEntity } from "../types.js";
export class StringBuilder extends Placeholder<string> {
public segments: (string | Placeholder<string>)[] = [];
#placeholders: Set<Placeholder<string>> = new Set();
#notifyComplete() {
const value = this.segments.join("");
this.setValue(value);
}
#setPlaceholderValue(ph: Placeholder<string>, value: string) {
for (const [i, segment] of this.segments.entries()) {
if (segment === ph) {
this.segments[i] = value;
}
}
this.#placeholders.delete(ph);
if (this.#placeholders.size === 0) {
this.#notifyComplete();
}
}
pushLiteralSegment(segment: string) {
if (this.#shouldConcatLiteral()) {
this.segments[this.segments.length - 1] += segment;
} else {
this.segments.push(segment);
}
}
pushPlaceholder(ph: Placeholder<string>) {
this.#placeholders.add(ph);
ph.onValue((value) => {
this.#setPlaceholderValue(ph, value);
});
this.segments.push(ph);
}
pushStringBuilder(builder: StringBuilder) {
for (const segment of builder.segments) {
this.push(segment);
}
}
push(segment: StringBuilder | Placeholder<string> | string) {
if (typeof segment === "string") {
this.pushLiteralSegment(segment);
} else if (segment instanceof StringBuilder) {
this.pushStringBuilder(segment);
} else {
this.pushPlaceholder(segment);
}
}
reduce() {
if (this.#placeholders.size === 0) {
return this.segments.join("");
}
return this;
}
#shouldConcatLiteral() {
return this.segments.length > 0 && typeof this.segments[this.segments.length - 1] === "string";
}
}
export function code(
parts: TemplateStringsArray,
...substitutions: (EmitEntity<string> | string | Placeholder<string> | StringBuilder)[]
): StringBuilder | string {
const builder = new StringBuilder();
for (const [i, literalPart] of parts.entries()) {
builder.push(literalPart);
if (i < substitutions.length) {
const sub = substitutions[i];
if (typeof sub === "string") {
builder.push(sub);
} else if (sub instanceof StringBuilder) {
builder.pushStringBuilder(sub);
} else if (sub instanceof Placeholder) {
builder.pushPlaceholder(sub);
} else {
switch (sub.kind) {
case "circular":
case "none":
builder.pushLiteralSegment("");
break;
default:
builder.push(sub.value);
}
}
}
}
return builder.reduce();
}

Просмотреть файл

@ -0,0 +1,53 @@
/**
* This is a map type that allows providing a custom keyer function. The keyer
* function returns a string that is used to look up in the map. This is useful
* for implementing maps that look up based on an arbitrary number of keys.
*
* For example, to look up in a map with a [ObjA, ObjB)] tuple, such that tuples
* with identical values (but not necessarily identical tuples!) create an
* object keyer for each of the objects:
*
* const aKeyer = CustomKeyMap.objectKeyer();
* const bKeyer = CUstomKeyMap.objectKeyer();
*
* And compose these into a tuple keyer to use when instantiating the custom key
* map:
*
* const tupleKeyer = ([a, b]) => `${aKeyer.getKey(a)}-${bKeyer.getKey(b)}`;
* const map = new CustomKeyMap(tupleKeyer);
*
*/
export class CustomKeyMap<K extends readonly any[], V> {
#items = new Map<string, V>();
#keyer;
constructor(keyer: (args: K) => string) {
this.#keyer = keyer;
}
get(items: K): V | undefined {
return this.#items.get(this.#keyer(items));
}
set(items: K, value: V): void {
const key = this.#keyer(items);
this.#items.set(key, value);
}
static objectKeyer() {
const knownKeys = new WeakMap<object, number>();
let count = 0;
return {
getKey(o: object) {
if (knownKeys.has(o)) {
return knownKeys.get(o);
}
const key = count;
count++;
knownKeys.set(o, key);
return key;
},
};
}
}

Просмотреть файл

@ -0,0 +1,7 @@
export * from "./asset-emitter.js";
export * from "./builders/array-builder.js";
export * from "./builders/object-builder.js";
export * from "./builders/string-builder.js";
export * from "./placeholder.js";
export * from "./type-emitter.js";
export * from "./types.js";

Просмотреть файл

@ -0,0 +1,18 @@
/**
* Keeps track of a value we don't know yet because of a circular reference. Use
* the `onValue` method to provide a callback with how to handle the
* placeholder's value becoming available. Generally the callback will replace
* this placeholder with the value in whatever references the placeholder.
*/
export class Placeholder<T> {
#listeners: ((value: T) => void)[] = [];
setValue(value: T) {
for (const listener of this.#listeners) {
listener(value);
}
}
onValue(cb: (value: T) => void) {
this.#listeners.push(cb);
}
}

Просмотреть файл

@ -0,0 +1,729 @@
import {
BooleanLiteral,
compilerAssert,
Enum,
EnumMember,
Interface,
IntrinsicType,
isTemplateDeclaration,
Model,
ModelProperty,
Namespace,
NumericLiteral,
Operation,
Program,
Scalar,
StringLiteral,
Tuple,
Type,
Union,
UnionVariant,
} from "../core/index.js";
import { code, StringBuilder } from "./builders/string-builder.js";
import { Placeholder } from "./placeholder.js";
import {
AssetEmitter,
CadlDeclaration,
Context,
Declaration,
EmitEntity,
EmittedSourceFile,
Scope,
SourceFile,
} from "./types.js";
export type EmitterOutput<T> = EmitEntity<T> | Placeholder<T> | T;
/**
* Implement emitter logic by extending this class and passing it to
* `emitContext.createAssetEmitter`. This class should not be constructed
* directly.
*
* TypeEmitters serve two primary purposes:
*
* 1. Handle emitting TypeSpec types into other languages
* 2. Set emitter context
*
* The generic type parameter `T` is the type you expect to produce for each TypeSpec type.
* In the case of generating source code for a programming language, this is probably `string`
* (in which case, consider using the `CodeTypeEmitter`) but might also be an AST node. If you
* are emitting JSON or similar, `T` would likely be `object`.
*
* ## Emitting types
*
* Emitting TypeSpec types into other languages is accomplished by implementing
* the AssetEmitter method that corresponds with the TypeSpec type you are
* emitting. For example, to emit a TypeSpec model declaration, implement the
* `modelDeclaration` method.
*
* TypeSpec types that have both declaration and literal forms like models or
* unions will have separate methods. For example, models have both
* `modelDeclaration` and `modelLiteral` methods that can be implemented
* separately.
*
* Also, types which can be instantiated like models or operations have a
* separate method for the instantiated type. For example, models have a
* `modelInstantiation` method that gets called with such types. Generally these
* will be treated either as if they were declarations or literals depending on
* preference, but may also be treated specially.
*
* ## Emitter results
* There are three kinds of results your methods might return - declarations,
* raw code, or nothing.
*
* ### Declarations
*
* Create declarations by calling `this.emitter.result.declaration` passing it a
* name and the emit output for the declaration. Note that you must have scope
* in your context or you will get an error. If you want all declarations to be
* emitted to the same source file, you can create a single scope in
* `programContext` via something like:
*
* ```typescript
* programContext(program: Program): Context {
* const sourceFile = this.emitter.createSourceFile("test.txt");
* return {
* scope: sourceFile.globalScope,
* };
* }
* ```
*
* ### Raw Code
*
* Create raw code, or emitter output that doesn't contribute to a declaration,
* by calling `this.emitter.result.rawCode` passing it a value. Returning just a
* value is considered raw code and so you often don't need to call this
* directly.
*
* ### No Emit
*
* When a type doesn't contribute anything to the emitted output, return
* `this.emitter.result.none()`.
*
* ## Context
*
* The TypeEmitter will often want to keep track of what context a type is found
* in. There are two kinds of context - lexical context, and reference context.
*
* * Lexical context is context that applies to the type and every type
* contained inside of it. For example, lexical context for a model will apply
* to the model, its properties, and any nested model literals.
* * Reference context is context that applies to types contained inside of the
* type and referenced anywhere inside of it. For example, reference context
* set on a model will apply to the model, its properties, any nested model
* literals, and any type referenced inside anywhere inside the model and any
* of the referenced types' references.
*
* In both cases, context is an object. It strongly recommended that the context
* object either contain only primitive types, or else only reference immutable
* objects.
*
* Set lexical by implementing the `*Context` methods of the TypeEmitter and
* returning the context, for example `modelDeclarationContext` sets the context
* for model declarations and the types contained inside of it.
*
* Set reference context by implementing the `*ReferenceContext` methods of the
* TypeEmitter and returning the context. Note that not all types have reference
* context methods, because not all types can actually reference anything.
*
* When a context method returns some context, it is merged with the current
* context. It is not possible to remove previous context, but it can be
* overridden with `undefined`.
*
* When emitting types with context, the same type might be emitted multiple
* times if we come across that type with different contexts. For example, if we
* have a TypeSpec program like
*
* ```cadl
* model Pet { }
* model Person {
* pet: Pet;
* }
* ```
*
* And we set reference context for the Person model, Pet will be emitted twice,
* once without context and once with the reference context.
*/
export class TypeEmitter<T> {
/**
* @private
*
* Constructs a TypeEmitter. Do not use this constructor directly, instead
* call `createAssetEmitter` on the emitter context object.
* @param emitter The asset emitter
*/
constructor(protected emitter: AssetEmitter<T>) {}
/**
* Context shared by the entire program. In cases where you are emitting to a
* single file, use this method to establish your main source file and set the
* `scope` property to that source file's `globalScope`.
* @param program
* @returns Context
*/
programContext(program: Program): Context {
return {};
}
/**
* Emit a namespace
*
* @param namespace
* @returns Emitter output
*/
namespace(namespace: Namespace): EmitterOutput<T> {
for (const ns of namespace.namespaces.values()) {
this.emitter.emitType(ns);
}
for (const model of namespace.models.values()) {
if (!isTemplateDeclaration(model)) {
this.emitter.emitType(model);
}
}
for (const operation of namespace.operations.values()) {
if (!isTemplateDeclaration(operation)) {
this.emitter.emitType(operation);
}
}
for (const enumeration of namespace.enums.values()) {
this.emitter.emitType(enumeration);
}
for (const union of namespace.unions.values()) {
if (!isTemplateDeclaration(union)) {
this.emitter.emitType(union);
}
}
for (const iface of namespace.interfaces.values()) {
if (!isTemplateDeclaration(iface)) {
this.emitter.emitType(iface);
}
}
return this.emitter.result.none();
}
/**
* Set lexical context for a namespace
*
* @param namespace
*/
namespaceContext(namespace: Namespace): Context {
return {};
}
/**
* Set reference context for a namespace.
*
* @param namespace
*/
namespaceReferenceContext(namespace: Namespace): Context {
return {};
}
/**
* Emit a model literal (e.g. as created by `{}` syntax in TypeSpec).
*
* @param model
*/
modelLiteral(model: Model): EmitterOutput<T> {
if (model.baseModel) {
this.emitter.emitType(model.baseModel);
}
this.emitter.emitModelProperties(model);
return this.emitter.result.none();
}
/**
* Set lexical context for a model literal.
* @param model
*/
modelLiteralContext(model: Model): Context {
return {};
}
/**
* Set reference context for a model literal.
* @param model
*/
modelLiteralReferenceContext(model: Model): Context {
return {};
}
/**
* Emit a model declaration (e.g. as created by `model Foo { }` syntax in
* TypeSpec).
*
* @param model
*/
modelDeclaration(model: Model, name: string): EmitterOutput<T> {
if (model.baseModel) {
this.emitter.emitType(model.baseModel);
}
this.emitter.emitModelProperties(model);
return this.emitter.result.none();
}
/**
* Set lexical context for a model declaration.
*
* @param model
* @param name the model's declaration name as retrieved from the
* `declarationName` method.
*/
modelDeclarationContext(model: Model, name: string): Context {
return {};
}
/**
* Set reference context for a model declaration.
* @param model
*/
modelDeclarationReferenceContext(model: Model): Context {
return {};
}
/**
* Emit a model instantiation (e.g. as created by `Foo<string>` syntax in
* TypeSpec).
*
* @param model
* @param name The name of the instantiation as retrieved from the
* `declarationName` method.
*/
modelInstantiation(model: Model, name: string): EmitterOutput<T> {
if (model.baseModel) {
this.emitter.emitType(model.baseModel);
}
this.emitter.emitModelProperties(model);
return this.emitter.result.none();
}
/**
* Set lexical context for a model instantiation.
* @param model
*/
modelInstantiationContext(model: Model): Context {
return {};
}
/**
* Set reference context for a model declaration.
* @param model
*/
modelInstantiationReferenceContext(model: Model): Context {
return {};
}
/**
* Emit a model's properties. Unless overridden, this method will emit each of
* the model's properties and return a no emit result.
*
* @param model
*/
modelProperties(model: Model): EmitterOutput<T> {
for (const prop of model.properties.values()) {
this.emitter.emitModelProperty(prop);
}
return this.emitter.result.none();
}
/**
* Emit a property of a model.
*
* @param property
*/
modelPropertyLiteral(property: ModelProperty): EmitterOutput<T> {
this.emitter.emitTypeReference(property.type);
return this.emitter.result.none();
}
/**
* Set lexical context for a property of a model.
*
* @param property
*/
modelPropertyLiteralContext(property: ModelProperty): Context {
return {};
}
/**
* Set reference context for a property of a model.
*
* @param property
*/
modelPropertyLiteralReferenceContext(property: ModelProperty): Context {
return {};
}
/**
* Emit a model property reference (e.g. as created by the `SomeModel.prop`
* syntax in TypeSpec). By default, this will emit the type of the referenced
* property and return that result. In other words, the emit will look as if
* `SomeModel.prop` were replaced with the type of `prop`.
*
* @param property
*/
modelPropertyReference(property: ModelProperty): EmitterOutput<T> {
return this.emitter.emitTypeReference(property.type);
}
scalarDeclaration(scalar: Scalar, name: string): EmitterOutput<T> {
if (scalar.baseScalar) {
this.emitter.emitType(scalar.baseScalar);
}
return this.emitter.result.none();
}
scalarDeclarationContext(scalar: Scalar): Context {
return {};
}
intrinsic(intrinsic: IntrinsicType, name: string): EmitterOutput<T> {
return this.emitter.result.none();
}
intrinsicContext(intrinsic: IntrinsicType): Context {
return {};
}
booleanLiteralContext(boolean: BooleanLiteral): Context {
return {};
}
booleanLiteral(boolean: BooleanLiteral): EmitterOutput<T> {
return this.emitter.result.none();
}
stringLiteralContext(string: StringLiteral): Context {
return {};
}
stringLiteral(string: StringLiteral): EmitterOutput<T> {
return this.emitter.result.none();
}
numericLiteralContext(number: NumericLiteral): Context {
return {};
}
numericLiteral(number: NumericLiteral): EmitterOutput<T> {
return this.emitter.result.none();
}
operationDeclaration(operation: Operation, name: string): EmitterOutput<T> {
this.emitter.emitOperationParameters(operation);
this.emitter.emitOperationReturnType(operation);
return this.emitter.result.none();
}
operationDeclarationContext(operation: Operation): Context {
return {};
}
operationDeclarationReferenceContext(operation: Operation): Context {
return {};
}
operationParameters(operation: Operation, parameters: Model): EmitterOutput<T> {
return this.emitter.result.none();
}
operationParametersContext(operation: Operation, parameters: Model): Context {
return {};
}
operationParametersReferenceContext(operation: Operation, parameters: Model): Context {
return {};
}
operationReturnType(operation: Operation, returnType: Type): EmitterOutput<T> {
return this.emitter.result.none();
}
operationReturnTypeContext(operation: Operation, returnType: Type): Context {
return {};
}
operationReturnTypeReferenceContext(operation: Operation, returnType: Type): Context {
return {};
}
interfaceDeclaration(iface: Interface, name: string): EmitterOutput<T> {
this.emitter.emitInterfaceOperations(iface);
return this.emitter.result.none();
}
interfaceDeclarationContext(iface: Interface): Context {
return {};
}
interfaceDeclarationReferenceContext(iface: Interface): Context {
return {};
}
interfaceDeclarationOperations(iface: Interface): EmitterOutput<T> {
for (const op of iface.operations.values()) {
this.emitter.emitInterfaceOperation(op);
}
return this.emitter.result.none();
}
interfaceOperationDeclaration(operation: Operation, name: string): EmitterOutput<T> {
this.emitter.emitOperationParameters(operation);
this.emitter.emitOperationReturnType(operation);
return this.emitter.result.none();
}
interfaceOperationDeclarationContext(operation: Operation): Context {
return {};
}
interfaceOperationDeclarationReferenceContext(operation: Operation): Context {
return {};
}
enumDeclaration(en: Enum, name: string): EmitterOutput<T> {
this.emitter.emitEnumMembers(en);
return this.emitter.result.none();
}
enumDeclarationContext(en: Enum): Context {
return {};
}
enumMembers(en: Enum): EmitterOutput<T> {
for (const member of en.members.values()) {
this.emitter.emitType(member);
}
return this.emitter.result.none();
}
enumMember(member: EnumMember): EmitterOutput<T> {
return this.emitter.result.none();
}
enumMemberContext(member: EnumMember) {
return {};
}
unionDeclaration(union: Union, name: string): EmitterOutput<T> {
this.emitter.emitUnionVariants(union);
return this.emitter.result.none();
}
unionDeclarationContext(union: Union): Context {
return {};
}
unionDeclarationReferenceContext(union: Union): Context {
return {};
}
unionInstantiation(union: Union, name: string): EmitterOutput<T> {
this.emitter.emitUnionVariants(union);
return this.emitter.result.none();
}
unionInstantiationContext(union: Union): Context {
return {};
}
unionInstantiationReferenceContext(union: Union): Context {
return {};
}
unionLiteral(union: Union): EmitterOutput<T> {
this.emitter.emitUnionVariants(union);
return this.emitter.result.none();
}
unionLiteralContext(union: Union): Context {
return {};
}
unionLiteralReferenceContext(union: Union): Context {
return {};
}
unionVariants(union: Union): EmitterOutput<T> {
for (const variant of union.variants.values()) {
this.emitter.emitType(variant);
}
return this.emitter.result.none();
}
unionVariant(variant: UnionVariant): EmitterOutput<T> {
this.emitter.emitTypeReference(variant.type);
return this.emitter.result.none();
}
unionVariantContext(union: Union): Context {
return {};
}
unionVariantReferenceContext(union: Union): Context {
return {};
}
tupleLiteral(tuple: Tuple): EmitterOutput<T> {
this.emitter.emitTupleLiteralValues(tuple);
return this.emitter.result.none();
}
tupleLiteralContext(tuple: Tuple): Context {
return {};
}
tupleLiteralValues(tuple: Tuple): EmitterOutput<T> {
for (const value of tuple.values.values()) {
this.emitter.emitType(value);
}
return this.emitter.result.none();
}
tupleLiteralReferenceContext(tuple: Tuple): Context {
return {};
}
sourceFile(sourceFile: SourceFile<T>): EmittedSourceFile {
const emittedSourceFile: EmittedSourceFile = {
path: sourceFile.path,
contents: "",
};
for (const decl of sourceFile.globalScope.declarations) {
emittedSourceFile.contents += decl.value + "\n";
}
return emittedSourceFile;
}
reference(
targetDeclaration: Declaration<T>,
pathUp: Scope<T>[],
pathDown: Scope<T>[],
commonScope: Scope<T> | null
): EmitEntity<T> | T {
return this.emitter.result.none();
}
declarationName(declarationType: CadlDeclaration): string {
compilerAssert(
declarationType.name !== undefined,
"Can't emit a declaration that doesn't have a name."
);
if (declarationType.kind === "Enum") {
return declarationType.name;
}
// for operations inside interfaces, we don't want to do the fancy thing because it will make
// operations inside instantiated interfaces get weird names
if (declarationType.kind === "Operation" && declarationType.interface) {
return declarationType.name;
}
if (!declarationType.templateMapper) {
return declarationType.name;
}
const parameterNames = declarationType.templateMapper.args.map((t) => {
switch (t.kind) {
case "Model":
case "Scalar":
case "Interface":
case "Operation":
case "Enum":
case "Union":
return this.emitter.emitDeclarationName(t);
default:
compilerAssert(
false,
`Can't get a declaration name for non-declaration type ${t.kind} used to instantiate a template.`
);
}
});
return declarationType.name + parameterNames.join("");
}
}
/**
* A subclass of `TypeEmitter<string>` that makes working with strings a bit easier.
* In particular, when emitting members of a type (`modelProperties`, `enumMembers`, etc.),
* instead of returning no result, it returns the value of each of the members concatenated
* by commas. It will also construct references by concatenating namespace elements together
* with `.` which should work nicely in many object oriented languages.
*/
export class CodeTypeEmitter extends TypeEmitter<string> {
modelProperties(model: Model): EmitterOutput<string> {
const builder = new StringBuilder();
let i = 0;
for (const prop of model.properties.values()) {
i++;
const propVal = this.emitter.emitModelProperty(prop);
builder.push(code`${propVal}${i < model.properties.size ? "," : ""}`);
}
return this.emitter.result.rawCode(builder.reduce());
}
interfaceDeclarationOperations(iface: Interface): EmitterOutput<string> {
const builder = new StringBuilder();
let i = 0;
for (const op of iface.operations.values()) {
i++;
builder.push(
code`${this.emitter.emitInterfaceOperation(op)}${i < iface.operations.size ? "," : ""}`
);
}
return builder.reduce();
}
enumMembers(en: Enum): EmitterOutput<string> {
const builder = new StringBuilder();
let i = 0;
for (const enumMember of en.members.values()) {
i++;
builder.push(code`${this.emitter.emitType(enumMember)}${i < en.members.size ? "," : ""}`);
}
return builder.reduce();
}
unionVariants(union: Union): EmitterOutput<string> {
const builder = new StringBuilder();
let i = 0;
for (const v of union.variants.values()) {
i++;
builder.push(code`${this.emitter.emitType(v)}${i < union.variants.size ? "," : ""}`);
}
return builder.reduce();
}
tupleLiteralValues(tuple: Tuple): EmitterOutput<string> {
const builder = new StringBuilder();
let i = 0;
for (const v of tuple.values) {
i++;
``;
builder.push(code`${this.emitter.emitTypeReference(v)}${i < tuple.values.length ? "," : ""}`);
}
return builder.reduce();
}
reference(
targetDeclaration: Declaration<string>,
pathUp: Scope<string>[],
pathDown: Scope<string>[],
commonScope: Scope<string> | null
): string | EmitEntity<string> {
const basePath = pathDown.map((s) => s.name).join(".");
return basePath
? this.emitter.result.rawCode(basePath + "." + targetDeclaration.name)
: this.emitter.result.rawCode(targetDeclaration.name);
}
}

Просмотреть файл

@ -0,0 +1,148 @@
import {
Enum,
Interface,
Model,
ModelProperty,
Operation,
Program,
Scalar,
Tuple,
Type,
Union,
} from "../core/index.js";
import { Placeholder } from "./placeholder.js";
export interface AssetEmitter<T> {
/**
* Get the current emitter context as set by the TypeEmitter's various
* context methods.
*
* @returns The current emitter context
*/
getContext(): Context;
getOptions(): Record<string, unknown>;
getProgram(): Program;
emitTypeReference(type: Type): EmitEntity<T>;
emitDeclarationName(type: CadlDeclaration): string;
emitType(type: Type): EmitEntity<T>;
emitProgram(options?: { emitGlobalNamespace?: boolean; emitCadlNamespace?: boolean }): void;
emitModelProperties(model: Model): EmitEntity<T>;
emitModelProperty(prop: ModelProperty): EmitEntity<T>;
emitOperationParameters(operation: Operation): EmitEntity<T>;
emitOperationReturnType(operation: Operation): EmitEntity<T>;
emitInterfaceOperations(iface: Interface): EmitEntity<T>;
emitInterfaceOperation(operation: Operation): EmitEntity<T>;
emitEnumMembers(en: Enum): EmitEntity<T>;
emitUnionVariants(union: Union): EmitEntity<T>;
emitTupleLiteralValues(tuple: Tuple): EmitEntity<T>;
createSourceFile(name: string): SourceFile<T>;
createScope(sourceFile: SourceFile<T>, name: string): SourceFileScope<T>;
createScope(namespace: any, name: string, parentScope: Scope<T>): NamespaceScope<T>;
createScope(block: any, name: string, parentScope?: Scope<T> | null): Scope<T>;
result: {
declaration(name: string, value: T | Placeholder<T>): Declaration<T>;
rawCode(value: T | Placeholder<T>): RawCode<T>;
none(): NoEmit;
};
writeOutput(): Promise<void>;
}
export interface ScopeBase<T> {
kind: string;
name: string;
parentScope: Scope<T> | null;
childScopes: Scope<T>[];
declarations: Declaration<T>[];
}
export interface SourceFileScope<T> extends ScopeBase<T> {
kind: "sourceFile";
sourceFile: SourceFile<T>;
}
export interface NamespaceScope<T> extends ScopeBase<T> {
kind: "namespace";
parentScope: Scope<T>;
namespace: any;
}
export type Scope<T> = SourceFileScope<T> | NamespaceScope<T>;
export interface TypeReference {
expression: string;
}
export interface SourceFile<T> {
path: string;
globalScope: Scope<T>;
imports: Map<string, string[]>;
}
export interface EmittedSourceFile {
contents: string;
path: string;
}
export type EmitEntity<T> = Declaration<T> | RawCode<T> | NoEmit | CircularEmit;
export class EmitterResult {}
export class Declaration<T> extends EmitterResult {
public kind = "declaration" as const;
constructor(public name: string, public scope: Scope<T>, public value: T | Placeholder<T>) {
if (value instanceof Placeholder) {
value.onValue((v) => (this.value = v));
}
super();
}
}
export class RawCode<T> extends EmitterResult {
public kind = "code" as const;
constructor(public value: T | Placeholder<T>) {
if (value instanceof Placeholder) {
value.onValue((v) => (this.value = v));
}
super();
}
}
export class NoEmit extends EmitterResult {
public kind = "none" as const;
}
export class CircularEmit extends EmitterResult {
public kind = "circular" as const;
constructor(public emitEntityKey: [string, Type, ContextState]) {
super();
}
}
export interface AssetTag {
language: AssetTagFactory;
create(key: string): AssetTagFactory;
}
export interface AssetTagInstance {}
export type AssetTagFactory = {
(value: string): AssetTagInstance;
};
export type CadlDeclaration = Model | Interface | Union | Operation | Enum | Scalar;
export interface ContextState {
lexicalContext: Record<string, any>;
referenceContext: Record<string, any>;
}
export type Context = Record<string, any>;
export type ESRecord = Record<string, any> & { _record: true };
export interface EmitterState {
lexicalTypeStack: Type[];
context: ContextState;
}

Просмотреть файл

@ -23,7 +23,8 @@
"exports": {
".": "./dist/core/index.js",
"./testing": "./dist/testing/index.js",
"./module-resolver": "./dist/core/module-resolver.js"
"./module-resolver": "./dist/core/module-resolver.js",
"./emitter-framework": "./dist/emitter-framework/index.js"
},
"browser": {
"./dist/core/node-host.js": "./dist/core/node-host.browser.js",
@ -111,6 +112,8 @@
"tmlanguage-generator": "~0.3.2",
"typescript": "~4.9.3",
"vscode-oniguruma": "~1.6.1",
"vscode-textmate": "~8.0.0"
"vscode-textmate": "~8.0.0",
"sinon": "~15.0.1",
"@types/sinon": "~10.0.13"
}
}

Просмотреть файл

@ -0,0 +1,280 @@
import assert from "assert";
import { Model, ModelProperty, Namespace, Program } from "../../core/index.js";
import { CodeTypeEmitter, Context, EmitterOutput } from "../../emitter-framework/index.js";
import { emitCadl } from "./host.js";
describe("emitter context", () => {
describe("program context", () => {
it("should be initialized to empty state", async () => {
class Emitter extends CodeTypeEmitter {
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const context = this.emitter.getContext();
assert.deepStrictEqual(context, {});
return super.modelDeclaration(model, name);
}
}
await emitCadl(Emitter, `model Foo { }`);
});
it("should set program state for the whole program", async () => {
class Emitter extends CodeTypeEmitter {
programContext(program: Program) {
return {
inProgram: true,
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const context = this.emitter.getContext();
assert.deepStrictEqual(context, { inProgram: true });
return super.modelDeclaration(model, name);
}
}
await emitCadl(Emitter, `model Foo { }`);
});
});
describe("namespace context", () => {
it("should set context for everything inside the namespace", async () => {
class Emitter extends CodeTypeEmitter {
namespaceContext(namespace: Namespace): Context {
return { inNamespace: true };
}
namespace(namespace: Namespace): EmitterOutput<string> {
assert.deepStrictEqual(this.emitter.getContext(), {
inNamespace: true,
});
return super.namespace(namespace);
}
}
await emitCadl(Emitter, `namespace Foo { }`);
});
it("should set context for everything inside the namespace, multiple namespaces", async () => {
class Emitter extends CodeTypeEmitter {
namespaceContext(namespace: Namespace): Context {
return { inNamespace: namespace.name };
}
namespace(namespace: Namespace): EmitterOutput<string> {
assert.deepStrictEqual(this.emitter.getContext(), {
inNamespace: namespace.name,
});
return super.namespace(namespace);
}
}
await emitCadl(Emitter, `namespace Foo { } namespace Bar { }`, {
namespaceContext: 2,
namespace: 2,
});
});
it("should set context for everything inside the namespace, nested namespaces", async () => {
class Emitter extends CodeTypeEmitter {
namespaceContext(namespace: Namespace): Context {
const newState: Record<string, boolean> = {};
if (namespace.name === "Foo") {
newState.foo = true;
} else {
newState.bar = true;
}
return newState;
}
namespace(namespace: Namespace): EmitterOutput<string> {
const expectedContext: Record<string, boolean> = { foo: true };
if (namespace.name === "Bar") {
expectedContext.bar = true;
}
assert.deepStrictEqual(
this.emitter.getContext(),
expectedContext,
"context for namespace " + namespace.name
);
return super.namespace(namespace);
}
}
await emitCadl(Emitter, `namespace Foo { namespace Bar { } }`, {
namespaceContext: 2,
namespace: 2,
});
});
});
describe("model context", () => {
it("sets model context for models and properties", async () => {
class Emitter extends CodeTypeEmitter {
modelDeclarationContext(model: Model, name: string): Context {
return {
inModel: true,
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
assert.deepStrictEqual(this.emitter.getContext(), {
inModel: true,
});
return super.modelDeclaration(model, name);
}
modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
assert.deepStrictEqual(this.emitter.getContext(), {
inModel: true,
});
return super.modelPropertyLiteral(property);
}
}
await emitCadl(
Emitter,
`model Foo {
prop: string;
}`
);
});
it("sets model context for nested model literals", async () => {
class Emitter extends CodeTypeEmitter {
modelDeclarationContext(model: Model, name: string): Context {
return {
inModel: true,
};
}
modelLiteral(model: Model): EmitterOutput<string> {
assert.deepStrictEqual(this.emitter.getContext(), {
inModel: true,
});
return super.modelLiteral(model);
}
}
await emitCadl(
Emitter,
`model Foo {
prop: {
nested: true
};
}`
);
});
});
describe("references", () => {
it("namespace context is preserved for models in that namespace even with references", async () => {
class TestEmitter extends CodeTypeEmitter {
namespaceContext(namespace: Namespace): Context {
return {
inANamespace: namespace.name === "A",
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const context = this.emitter.getContext();
if (name === "Foo") {
assert(context.inANamespace);
} else {
assert(!context.inANamespace);
}
return super.modelDeclaration(model, name);
}
}
await emitCadl(
TestEmitter,
`
model Bar { prop: A.Foo };
namespace A {
model Foo { prop: string };
}
`,
{
namespaceContext: 2,
modelDeclaration: 2,
}
);
});
});
describe("reference context", () => {
it("propagates reference context", async () => {
const seenContexts: Set<boolean> = new Set();
class TestEmitter extends CodeTypeEmitter {
namespaceReferenceContext(namespace: Namespace): Context {
if (namespace.name === "Foo") {
return { refFromNs: true };
}
return {};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const context = this.emitter.getContext();
if (model.name === "N") {
seenContexts.add(context.refFromNs ?? false);
}
return super.modelDeclaration(model, name);
}
}
await emitCadl(
TestEmitter,
`
namespace Foo {
model M { x: Bar.N }
}
namespace Bar {
model N {}
}
`,
{
namespaceReferenceContext: 2,
modelDeclaration: 3,
}
);
assert(seenContexts.has(true), "N has ref context");
assert(seenContexts.has(false), "N doesn't ref context also");
});
it("doesn't emit model multiple times when reference context is the same", async () => {
class TestEmitter extends CodeTypeEmitter {
modelDeclarationReferenceContext(model: Model): Context {
if (model.name === "Qux") {
return {};
}
return { ref: true };
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
return super.modelDeclaration(model, name);
}
}
await emitCadl(
TestEmitter,
`
model Foo { x: Qux }
model Bar { x: Qux }
model Qux { }
`,
{
modelDeclarationReferenceContext: 4,
modelDeclaration: 4,
}
);
});
});
});

Просмотреть файл

@ -0,0 +1,644 @@
import assert from "assert";
import prettier from "prettier";
import {
Enum,
Interface,
Model,
ModelProperty,
Operation,
Program,
Type,
Union,
} from "../../core/index.js";
import {
ArrayBuilder,
AssetEmitter,
CadlDeclaration,
code,
CodeTypeEmitter,
Context,
createAssetEmitter,
Declaration,
EmitEntity,
EmittedSourceFile,
EmitterOutput,
ObjectBuilder,
Placeholder,
Scope,
SourceFile,
StringBuilder,
TypeEmitter,
} from "../../emitter-framework/index.js";
import { emitCadl, getHostForCadlFile } from "./host.js";
import { TypeScriptInterfaceEmitter } from "./typescript-emitter.js";
const testCode = `
model Basic { x: string }
model RefsOtherModel { x: Basic, y: UnionDecl }
model HasNestedLiteral { x: { y: string } }
model HasArrayProperty { x: string[], y: Basic[] }
model IsArray is Array<string>;
model Derived extends Basic { }
@doc("Has a doc")
model HasDoc { @doc("an x property") x: string }
model Template<T> { prop: T }
model HasTemplates { x: Template<Basic> }
model IsTemplate is Template<Basic>;
model HasRef {
x: Basic.x;
y: RefsOtherModel.x;
}
op SomeOp(x: string): string;
interface MyInterface {
op get(): string;
}
union UnionDecl {
x: int32;
y: string;
}
enum MyEnum {
a: "hi";
b: "bye";
}
`;
class SingleFileEmitter extends TypeScriptInterfaceEmitter {
programContext(): Context {
const outputFile = this.emitter.createSourceFile("cadl-output/output.ts");
return { scope: outputFile.globalScope };
}
operationReturnTypeReferenceContext(operation: Operation, returnType: Type): Context {
return {
fromOperation: true,
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const newName = this.emitter.getContext().fromOperation ? name + "FromOperation" : name;
return super.modelDeclaration(model, newName);
}
}
async function emitCadlToTs(code: string) {
const emitter = await emitCadl(SingleFileEmitter, code, {}, false);
const sf = await emitter.getProgram().host.readFile("./cadl-output/output.ts");
return sf.text;
}
describe("typescript emitter", () => {
it("emits models", async () => {
const contents = await emitCadlToTs(`
model A {
x: {
y: string;
},
}
`);
assert.match(contents, /export interface A/);
assert.match(contents, /x: \{ y: string \}/);
});
it("emits model templates", async () => {
const contents = await emitCadlToTs(`
model Template<T> {
x: T
}
model Test1 is Template<string>;
model Test2 {
prop: Template<int32>;
}
`);
assert.match(contents, /interface Test1/);
assert.match(contents, /interface Templateint32/);
assert.match(contents, /interface Test2/);
assert.match(contents, /prop: Templateint32/);
});
it("emits literal types", async () => {
const contents = await emitCadlToTs(`
model A {
x: true,
y: "hi",
z: 12
}
`);
assert.match(contents, /x: true/);
assert.match(contents, /y: "hi"/);
assert.match(contents, /z: 12/);
});
// todo: what to do with optionals not at the end??
it("emits operations", async () => {
const contents = await emitCadlToTs(`
model SomeModel {
x: string;
}
op read(x: string, y: int32, z: { inline: true }, q?: SomeModel): string;
`);
assert.match(contents, /interface read/);
assert.match(contents, /x: string/);
assert.match(contents, /y: number/);
assert.match(contents, /z: { inline: true }/);
assert.match(contents, /q?: SomeModel/);
});
it("emits interfaces", async () => {
const contents = await emitCadlToTs(`
model Foo {
prop: string;
}
op Callback(x: string): string;
interface Things {
op read(x: string): string;
op write(y: Foo): Foo;
op callCb(cb: Callback): string;
}
interface Template<T> {
op read(): T;
op write(): T;
}
interface TemplateThings extends Template<string> {}
`);
assert.match(contents, /export interface Things/);
assert.match(contents, /read\(x: string\): string/);
assert.match(contents, /write\(y: Foo\): Foo/);
assert.match(contents, /callCb\(cb: Callback\): string/);
assert.match(contents, /export interface TemplateThings/);
assert.match(contents, /read\(\): string/);
assert.match(contents, /write\(\): string/);
});
it("emits enums", async () => {
const contents = await emitCadlToTs(`
enum StringEnum {
x; y: "hello";
}
enum NumberEnum {
x: 1;
y: 2;
z: 3;
}
`);
assert.match(contents, /enum StringEnum/);
assert.match(contents, /x = "x"/);
assert.match(contents, /y = "hello"/);
assert.match(contents, /x = 1/);
});
it("emits unions", async () => {
const contents = await emitCadlToTs(`
model SomeModel {
a: 1 | 2 | SomeModel;
b: TU<string>;
};
union U {
x: 1,
y: "hello",
z: SomeModel
}
union TU<T> {
x: T;
y: null;
}
`);
assert.match(contents, /a: 1 \| 2 \| SomeModel/);
assert.match(contents, /b: TUstring/);
assert.match(contents, /export type U = 1 \| "hello" \| SomeModel/);
assert.match(contents, /export type TUstring = string \| null/);
});
it("emits tuple types", async () => {
const contents = await emitCadlToTs(`
model Foo {
x: [string, int32];
}
`);
assert.match(contents, /x: \[string, number\]/);
});
it("emits models to a single file", async () => {
const host = await getHostForCadlFile(testCode);
const emitter = createAssetEmitter(host.program, SingleFileEmitter);
emitter.emitProgram();
await emitter.writeOutput();
const files = await host.program.host.readDir("./cadl-output");
assert.strictEqual(files.length, 1);
const contents = (await host.program.host.readFile("./cadl-output/output.ts")).text;
// some light assertions
assert.match(contents, /export interface Basic/);
assert.match(contents, /export interface HasRef/);
});
it("emits to multiple files", async () => {
const host = await getHostForCadlFile(testCode);
class ClassPerFileEmitter extends TypeScriptInterfaceEmitter {
modelDeclarationContext(model: Model): Context {
return this.#declarationContext(model);
}
modelInstantiationContext(model: Model): Context {
return this.#declarationContext(model);
}
unionDeclarationContext(union: Union): Context {
return this.#declarationContext(union);
}
unionInstantiationContext(union: Union): Context {
return this.#declarationContext(union);
}
enumDeclarationContext(en: Enum): Context {
return this.#declarationContext(en);
}
interfaceDeclarationContext(iface: Interface): Context {
return this.#declarationContext(iface);
}
operationDeclarationContext(operation: Operation): Context {
return this.#declarationContext(operation);
}
#declarationContext(decl: CadlDeclaration) {
const name = this.emitter.emitDeclarationName(decl);
const outputFile = this.emitter.createSourceFile(`cadl-output/${name}.ts`);
return { scope: outputFile.globalScope };
}
}
const emitter = createAssetEmitter(host.program, ClassPerFileEmitter);
emitter.emitProgram();
await emitter.writeOutput();
const files = new Set(await host.program.host.readDir("./cadl-output"));
[
"Basic.ts",
"RefsOtherModel.ts",
"HasNestedLiteral.ts",
"HasArrayProperty.ts",
"IsArray.ts",
"Derived.ts",
"HasDoc.ts",
"HasTemplates.ts",
"TemplateBasic.ts",
"IsTemplate.ts",
"HasRef.ts",
"SomeOp.ts",
"MyEnum.ts",
"UnionDecl.ts",
"MyInterface.ts",
].forEach((file) => {
assert(files.has(file));
});
});
it("emits to namespaces", async () => {
const host = await getHostForCadlFile(testCode);
class NamespacedEmitter extends TypeScriptInterfaceEmitter {
private nsByName: Map<string, Scope<string>> = new Map();
programContext(program: Program): Context {
const outputFile = emitter.createSourceFile("output.ts");
return {
scope: outputFile.globalScope,
};
}
modelDeclarationContext(model: Model): Context {
const name = this.emitter.emitDeclarationName(model);
const nsName = name.slice(0, 1);
let nsScope = this.nsByName.get(nsName);
if (!nsScope) {
nsScope = this.emitter.createScope({}, nsName, this.emitter.getContext().scope);
this.nsByName.set(nsName, nsScope);
}
return {
scope: nsScope,
};
}
sourceFile(sourceFile: SourceFile<string>): EmittedSourceFile {
const emittedSourceFile = super.sourceFile(sourceFile);
emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
emittedSourceFile.contents = prettier.format(emittedSourceFile.contents, {
parser: "typescript",
});
return emittedSourceFile;
function emitNamespaces(scope: Scope<string>) {
let res = "";
for (const childScope of scope.childScopes) {
res += emitNamespace(childScope);
}
return res;
}
function emitNamespace(scope: Scope<string>) {
let ns = `namespace ${scope.name} {\n`;
ns += emitNamespaces(scope);
for (const decl of scope.declarations) {
ns += decl.value + "\n";
}
ns += `}\n`;
return ns;
}
}
}
const emitter = createAssetEmitter(host.program, NamespacedEmitter);
emitter.emitProgram();
await emitter.writeOutput();
const contents = (await host.compilerHost.readFile("output.ts")).text;
assert.match(contents, /namespace B/);
assert.match(contents, /namespace R/);
assert.match(contents, /namespace H/);
assert.match(contents, /namespace I/);
assert.match(contents, /namespace D/);
assert.match(contents, /B\.Basic/);
assert.match(contents, /B\.Basic/);
});
it("handles circular references", async () => {
const host = await getHostForCadlFile(`
model Foo { prop: Baz }
model Baz { prop: Foo }
`);
class SingleFileEmitter extends TypeScriptInterfaceEmitter {
programContext() {
const outputFile = emitter.createSourceFile("output.ts");
return { scope: outputFile.globalScope };
}
}
const emitter: AssetEmitter<string> = createAssetEmitter(host.program, SingleFileEmitter);
emitter.emitProgram();
await emitter.writeOutput();
const contents = (await host.compilerHost.readFile("output.ts")).text;
assert.match(contents, /prop: Foo/);
assert.match(contents, /prop: Baz/);
});
});
it("handles circular references", async () => {
let sourceFile: SourceFile<string>;
class TestEmitter extends CodeTypeEmitter {
programContext(program: Program): Context {
sourceFile = this.emitter.createSourceFile("hi.txt");
return {
scope: sourceFile.globalScope,
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const result = this.emitter.emitModelProperties(model);
return this.emitter.result.declaration(model.name, code`model references ${result}`);
}
modelProperties(model: Model): EmitterOutput<string> {
const builder = new StringBuilder();
for (const prop of model.properties.values()) {
builder.push(code`${this.emitter.emitModelProperty(prop)}`);
}
return this.emitter.result.rawCode(builder);
}
modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
return this.emitter.result.rawCode(code`${this.emitter.emitTypeReference(property.type)}`);
}
sourceFile(sourceFile: SourceFile<string>): EmittedSourceFile {
assert.strictEqual(sourceFile.globalScope.declarations.length, 2);
for (const decl of sourceFile.globalScope.declarations) {
if (decl.name === "Foo") {
assert.strictEqual(decl.value, "model references Bar");
} else {
assert.strictEqual(decl.value, "model references Foo");
}
}
return {
contents: "",
path: "",
};
}
}
await emitCadl(
TestEmitter,
`
model Bar { bProp: Foo };
model Foo { fProp: Bar };
`,
{
modelDeclaration: 2,
modelProperties: 2,
modelPropertyLiteral: 2,
}
);
});
it("handles multiple circular references", async () => {
let sourceFile: SourceFile<string>;
class TestEmitter extends CodeTypeEmitter {
programContext(program: Program): Context {
sourceFile = this.emitter.createSourceFile("hi.txt");
return {
scope: sourceFile.globalScope,
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
const result = this.emitter.emitModelProperties(model);
return this.emitter.result.declaration(model.name, code`model references ${result}`);
}
modelProperties(model: Model): EmitterOutput<string> {
const builder = new StringBuilder();
for (const prop of model.properties.values()) {
builder.push(code`${this.emitter.emitModelProperty(prop)}`);
}
return this.emitter.result.rawCode(builder);
}
modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
return this.emitter.result.rawCode(code`${this.emitter.emitTypeReference(property.type)}`);
}
sourceFile(sourceFile: SourceFile<string>): EmittedSourceFile {
assert.strictEqual(sourceFile.globalScope.declarations.length, 3);
for (const decl of sourceFile.globalScope.declarations) {
if (decl.name === "Foo") {
assert.strictEqual(decl.value, "model references BarBar");
} else if (decl.name === "Bar") {
assert.strictEqual(decl.value, "model references FooBaz");
} else if (decl.name === "Baz") {
assert.strictEqual(decl.value, "model references FooBar");
}
}
return {
contents: "",
path: "",
};
}
}
await emitCadl(
TestEmitter,
`
model Bar { prop: Foo, pro2: Baz };
model Foo { prop: Bar, prop2: Bar };
model Baz { prop: Foo, prop2: Bar };
`,
{
modelDeclaration: 3,
modelProperties: 3,
modelPropertyLiteral: 6,
}
);
});
it("can get options", async () => {
let called = false;
class TestEmitter extends CodeTypeEmitter {
programContext(program: Program) {
called = true;
assert.strictEqual(this.emitter.getOptions().doThing, "yes");
return {};
}
}
const host = await getHostForCadlFile(`model Foo { }`);
const assetEmitter = createAssetEmitter(host.program, TestEmitter, { doThing: "yes" });
assetEmitter.emitProgram();
assert(called, "program context should be called");
});
describe("Object emitter", () => {
class TestEmitter extends TypeEmitter<object> {
programContext(program: Program): Context {
const sourceFile = this.emitter.createSourceFile("test.json");
return {
scope: sourceFile.globalScope,
};
}
modelDeclaration(model: Model, name: string): EmitterOutput<object> {
const om = new ObjectBuilder({
kind: "model",
name,
members: this.emitter.emitModelProperties(model),
});
return this.emitter.result.declaration(name, om);
}
modelLiteral(model: Model): EmitterOutput<object> {
const om = new ObjectBuilder({
kind: "anonymous model",
members: this.emitter.emitModelProperties(model),
});
return om;
}
modelProperties(model: Model): EmitterOutput<object> {
const members = new ArrayBuilder();
for (const p of model.properties.values()) {
members.push(this.emitter.emitModelProperty(p));
}
return members;
}
modelPropertyLiteral(property: ModelProperty): EmitterOutput<object> {
const om = new ObjectBuilder({
kind: "modelProperty",
name: property.name,
type: this.emitter.emitTypeReference(property.type),
});
return om;
}
reference(
targetDeclaration: Declaration<object>,
pathUp: Scope<object>[],
pathDown: Scope<object>[],
commonScope: Scope<object> | null
): object | EmitEntity<object> {
return { $ref: targetDeclaration.name };
}
sourceFile(sourceFile: SourceFile<object>): EmittedSourceFile {
const emittedSourceFile: EmittedSourceFile = {
path: sourceFile.path,
contents: "",
};
const obj: { declarations: object[] } = { declarations: [] };
for (const decl of sourceFile.globalScope.declarations) {
if (decl.value instanceof Placeholder) {
obj.declarations.push({ placeholder: true });
} else {
obj.declarations.push(decl.value);
}
}
emittedSourceFile.contents = JSON.stringify(obj, null, 4);
return emittedSourceFile;
}
}
it("emits objects", async () => {
const host = await getHostForCadlFile(
`
model Foo {
bar: Bar
}
model Bar {
x: Foo;
y: {
x: Foo
};
};
`
);
const assetEmitter = createAssetEmitter(host.program, TestEmitter);
assetEmitter.emitProgram();
await assetEmitter.writeOutput();
const contents = JSON.parse((await host.compilerHost.readFile("test.json")!).text);
assert.strictEqual(contents.declarations.length, 2);
});
});

Просмотреть файл

@ -0,0 +1,85 @@
import { fileURLToPath } from "url";
import { resolvePath } from "../../core/index.js";
import { createAssetEmitter, TypeEmitter } from "../../emitter-framework/index.js";
import { CadlTestLibrary, createTestHost } from "../../testing/index.js";
import assert from "assert";
import { SinonSpy, spy } from "sinon";
export const lib: CadlTestLibrary = {
name: "cadl-ts-interface-emitter",
packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../"),
files: [
{
realDir: "",
pattern: "package.json",
virtualPath: "./node_modules/cadl-ts-interface-emitter",
},
{
realDir: "dist/src",
pattern: "*.js",
virtualPath: "./node_modules/cadl-ts-interface-emitter/dist/src",
},
],
};
export async function getHostForCadlFile(contents: string, decorators?: Record<string, any>) {
const host = await createTestHost();
if (decorators) {
await host.addJsFile("dec.js", decorators);
contents = `import "./dec.js";\n` + contents;
}
await host.addCadlFile("main.cadl", contents);
await host.compile("main.cadl", {
outputDir: "cadl-output",
});
return host;
}
export async function emitCadl(
Emitter: typeof TypeEmitter<any>,
code: string,
callCounts: Partial<Record<keyof TypeEmitter<any>, number>> = {},
validateCallCounts = true
) {
const host = await getHostForCadlFile(code);
const emitter = createAssetEmitter(host.program, Emitter);
const spies = emitterSpies(Emitter);
emitter.emitProgram();
await emitter.writeOutput();
if (validateCallCounts) {
assertSpiesCalled(spies, callCounts);
}
return emitter;
}
type EmitterSpies = Record<string, SinonSpy>;
function emitterSpies(emitter: typeof TypeEmitter) {
const spies: EmitterSpies = {};
const methods = Object.getOwnPropertyNames(emitter.prototype);
for (const key of methods) {
if (key === "constructor") continue;
if ((emitter.prototype as any)[key].restore) {
// assume this whole thing is already spied.
return spies;
}
if (typeof (emitter.prototype as any)[key] !== "function") continue;
spies[key] = spy(emitter.prototype, key as any);
}
return spies;
}
function assertSpiesCalled(
spies: EmitterSpies,
callCounts: Partial<Record<keyof TypeEmitter<any>, number>>
) {
for (const [key, spy] of Object.entries(spies)) {
const expectedCount = (callCounts as any)[key] ?? 1;
assert.equal(
spy.callCount,
expectedCount,
`Emitter method ${key} should called ${expectedCount} time(s), was called ${spy.callCount} time(s)`
);
}
}

Просмотреть файл

@ -0,0 +1,271 @@
import prettier from "prettier";
import {
BooleanLiteral,
Enum,
EnumMember,
getDoc,
Interface,
IntrinsicType,
Model,
ModelProperty,
NumericLiteral,
Operation,
Scalar,
StringLiteral,
Tuple,
Type,
Union,
UnionVariant,
} from "../../core/index.js";
import {
code,
CodeTypeEmitter,
Declaration,
EmittedSourceFile,
EmitterOutput,
Scope,
SourceFile,
SourceFileScope,
StringBuilder,
} from "../../emitter-framework/index.js";
export function isArrayType(m: Model) {
return m.name === "Array";
}
export const intrinsicNameToTSType = new Map<string, string>([
["string", "string"],
["int32", "number"],
["int16", "number"],
["float16", "number"],
["float32", "number"],
["int64", "bigint"],
["boolean", "boolean"],
["null", "null"],
]);
export class TypeScriptInterfaceEmitter extends CodeTypeEmitter {
// type literals
booleanLiteral(boolean: BooleanLiteral): EmitterOutput<string> {
return JSON.stringify(boolean.value);
}
numericLiteral(number: NumericLiteral): EmitterOutput<string> {
return JSON.stringify(number.value);
}
stringLiteral(string: StringLiteral): EmitterOutput<string> {
return JSON.stringify(string.value);
}
scalarDeclaration(scalar: Scalar, scalarName: string): EmitterOutput<string> {
if (!intrinsicNameToTSType.has(scalarName)) {
throw new Error("Unknown scalar type " + scalarName);
}
const code = intrinsicNameToTSType.get(scalarName)!;
return this.emitter.result.rawCode(code);
}
intrinsic(intrinsic: IntrinsicType, name: string): EmitterOutput<string> {
if (!intrinsicNameToTSType.has(name)) {
throw new Error("Unknown intrinsic type " + name);
}
const code = intrinsicNameToTSType.get(name)!;
return this.emitter.result.rawCode(code);
}
modelLiteral(model: Model): EmitterOutput<string> {
if (isArrayType(model)) {
return this.emitter.result.rawCode(
code`${this.emitter.emitTypeReference(model.indexer!.value!)}[]`
);
}
return this.emitter.result.rawCode(code`{ ${this.emitter.emitModelProperties(model)}}`);
}
modelDeclaration(model: Model, name: string): EmitterOutput<string> {
let extendsClause;
if (model.indexer && model.indexer.key!.name === "integer") {
extendsClause = code`extends Array<${this.emitter.emitTypeReference(model.indexer!.value!)}>`;
} else if (model.baseModel) {
extendsClause = code`extends ${this.emitter.emitTypeReference(model.baseModel)}`;
} else {
extendsClause = "";
}
const comment = getDoc(this.emitter.getProgram(), model);
let commentCode = "";
if (comment) {
commentCode = `
/**
* ${comment}
*/`;
}
return this.emitter.result.declaration(
name,
code`${commentCode}\nexport interface ${name} ${extendsClause} {
${this.emitter.emitModelProperties(model)}
}`
);
}
modelInstantiation(model: Model, name: string): EmitterOutput<string> {
return this.modelDeclaration(model, name);
}
modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
const name = property.name === "_" ? "statusCode" : property.name;
const doc = getDoc(this.emitter.getProgram(), property);
let docString = "";
if (doc) {
docString = `
/**
* ${doc}
*/
`;
}
return this.emitter.result.rawCode(
code`${docString}${name}${property.optional ? "?" : ""}: ${this.emitter.emitTypeReference(
property.type
)}`
);
}
operationDeclaration(operation: Operation, name: string): EmitterOutput<string> {
return this.emitter.result.declaration(
name,
code`interface ${name} {
${this.#operationSignature(operation)}
}`
);
}
operationParameters(operation: Operation, parameters: Model): EmitterOutput<string> {
const cb = new StringBuilder();
for (const prop of parameters.properties.values()) {
cb.push(
code`${prop.name}${prop.optional ? "?" : ""}: ${this.emitter.emitTypeReference(prop.type)},`
);
}
return cb;
}
#operationSignature(operation: Operation) {
return code`(${this.emitter.emitOperationParameters(
operation
)}): ${this.emitter.emitOperationReturnType(operation)}`;
}
operationReturnType(operation: Operation, returnType: Type): EmitterOutput<string> {
return this.emitter.emitTypeReference(returnType);
}
interfaceDeclaration(iface: Interface, name: string): EmitterOutput<string> {
return this.emitter.result.declaration(
name,
code`
export interface ${name} {
${this.emitter.emitInterfaceOperations(iface)}
}
`
);
}
interfaceOperationDeclaration(operation: Operation, name: string): EmitterOutput<string> {
return code`${name}${this.#operationSignature(operation)}`;
}
enumDeclaration(en: Enum, name: string): EmitterOutput<string> {
return this.emitter.result.declaration(
name,
code`export enum ${name} {
${this.emitter.emitEnumMembers(en)}
}`
);
}
enumMember(member: EnumMember): EmitterOutput<string> {
// should we just fill in value for you?
const value = !member.value ? member.name : member.value;
return `
${member.name} = ${JSON.stringify(value)}
`;
}
unionDeclaration(union: Union, name: string): EmitterOutput<string> {
return this.emitter.result.declaration(
name,
code`export type ${name} = ${this.emitter.emitUnionVariants(union)}`
);
}
unionInstantiation(union: Union, name: string): EmitterOutput<string> {
return this.unionDeclaration(union, name);
}
unionLiteral(union: Union) {
return this.emitter.emitUnionVariants(union);
}
unionVariants(union: Union): EmitterOutput<string> {
const builder = new StringBuilder();
let i = 0;
for (const variant of union.variants.values()) {
i++;
builder.push(code`${this.emitter.emitType(variant)}${i < union.variants.size ? "|" : ""}`);
}
return this.emitter.result.rawCode(builder.reduce());
}
unionVariant(variant: UnionVariant): EmitterOutput<string> {
return this.emitter.emitTypeReference(variant.type);
}
tupleLiteral(tuple: Tuple): EmitterOutput<string> {
return code`[${this.emitter.emitTupleLiteralValues(tuple)}]`;
}
reference(
targetDeclaration: Declaration<string>,
pathUp: Scope<string>[],
pathDown: Scope<string>[],
commonScope: Scope<string> | null
) {
if (!commonScope) {
const sourceSf = (pathUp[0] as SourceFileScope<string>).sourceFile;
const targetSf = (pathDown[0] as SourceFileScope<string>).sourceFile;
sourceSf.imports.set(`./${targetSf.path.replace(".js", ".ts")}`, [targetDeclaration.name]);
}
return super.reference(targetDeclaration, pathUp, pathDown, commonScope);
}
sourceFile(sourceFile: SourceFile<string>): EmittedSourceFile {
const emittedSourceFile: EmittedSourceFile = {
path: sourceFile.path,
contents: "",
};
for (const [importPath, typeNames] of sourceFile.imports) {
emittedSourceFile.contents += `import {${typeNames.join(",")}} from "${importPath}";\n`;
}
for (const decl of sourceFile.globalScope.declarations) {
emittedSourceFile.contents += decl.value + "\n";
}
emittedSourceFile.contents = prettier.format(emittedSourceFile.contents, {
parser: "typescript",
});
return emittedSourceFile;
}
}

Просмотреть файл

@ -108,6 +108,7 @@ const sidebars = {
"extending-cadl/create-decorators",
"extending-cadl/linters",
"extending-cadl/emitters",
"extending-cadl/emitter-framework",
"extending-cadl/writing-scaffolding-template",
],
},