Skip to content

Commit

Permalink
minor(emitter-typescript): update types to use proper namespacing
Browse files Browse the repository at this point in the history
  • Loading branch information
codingmatty committed Sep 16, 2024
1 parent d503233 commit ec989ea
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 120 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-gorillas-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typespec-tools/emitter-typescript": minor
---

Update types to use proper namespacing
104 changes: 104 additions & 0 deletions packages/emitter-typescript/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IntrinsicType,
Model,
ModelProperty,
Namespace,
NumericLiteral,
Operation,
Scalar,
Expand All @@ -31,6 +32,7 @@ import {
SourceFile,
SourceFileScope,
StringBuilder,
TypeSpecDeclaration,
} from "@typespec/compiler/emitter-framework";
import { EmitterOptions } from "./lib.js";

Expand All @@ -52,9 +54,109 @@ export const intrinsicNameToTSType = new Map<string, string>([
["void", "void"],
]);

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;
}

function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
let ns = [decl.namespace?.name];
let parent = decl.namespace?.namespace;
while (parent) {
ns.push(parent.name);
parent = parent.namespace;
}
return ns.filter((n): n is string => !!n).reverse();
}

export class TypescriptEmitter<
TEmitterOptions extends object = EmitterOptions,
> extends CodeTypeEmitter<TEmitterOptions> {
private nsByName: Map<string, Scope<string>> = new Map();

declarationContext(
decl: TypeSpecDeclaration & { namespace?: Namespace }
): Context {
const name = decl.namespace?.name;
if (!name) return {};

const namespaceChain = getNamespaceChain(decl);

let nsScope = this.nsByName.get(name);
if (!nsScope) {
// If there is no scope for the namespace, create one for each
// namespace in the chain.
let parentScope: Scope<string> | undefined;
while (namespaceChain.length > 0) {
const ns = namespaceChain.shift();
if (!ns) {
break;
}
nsScope = this.nsByName.get(ns);
if (nsScope) {
parentScope = nsScope;
continue;
}
nsScope = this.emitter.createScope(
{},
ns,
parentScope ?? this.emitter.getContext().scope
);
this.nsByName.set(ns, nsScope);
parentScope = nsScope;
}
}

return {
scope: nsScope,
};
}

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);
}

arrayDeclarationContext(array: Model): Context {
return this.declarationContext(array);
}

interfaceDeclarationContext(iface: Interface): Context {
return this.declarationContext(iface);
}

operationDeclarationContext(operation: Operation): Context {
return this.declarationContext(operation);
}

// type literals
booleanLiteral(boolean: BooleanLiteral): EmitterOutput<string> {
return JSON.stringify(boolean.value);
Expand Down Expand Up @@ -312,6 +414,8 @@ export class TypescriptEmitter<
emittedSourceFile.contents += decl.value + "\n";
}

emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);

emittedSourceFile.contents = await prettier.format(
emittedSourceFile.contents,
{
Expand Down
141 changes: 21 additions & 120 deletions packages/emitter-typescript/test/emitter.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { Enum, Interface, Model, Operation, Union } from "@typespec/compiler";
import { Model } from "@typespec/compiler";
import {
AssetEmitter,
Context,
EmittedSourceFile,
EmitterOutput,
Scope,
SourceFile,
TypeSpecDeclaration,
createAssetEmitter,
} from "@typespec/compiler/emitter-framework";

import assert from "assert";
import * as prettier from "prettier";
import { describe, it } from "vitest";

import {
Expand All @@ -22,28 +18,36 @@ import { EmitterOptions } from "../src/lib.js";
import { emitTypeSpec, getHostForTypeSpecFile } from "./host.js";

const testCode = `
namespace Root;
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 }
namespace WrappedModels {
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;
z: Operations.SomeOp;
}
op SomeOp(x: string): string;
namespace Operations {
op SomeOp(x: string): string;
interface MyInterface {
op get(): string;
interface MyInterface {
op get(): string;
}
}
union UnionDecl {
Expand All @@ -58,11 +62,6 @@ enum MyEnum {
`;

class SingleFileTestEmitter extends SingleFileTypescriptEmitter {
programContext(): Context {
const outputFile = this.emitter.createSourceFile("output.ts");
return { scope: outputFile.globalScope };
}

operationReturnTypeReferenceContext(): Context {
return {
fromOperation: true,
Expand Down Expand Up @@ -351,39 +350,7 @@ describe("emitter-framework: typescript emitter", () => {
const host = await getHostForTypeSpecFile(testCode);

class ClassPerFileEmitter extends TypescriptEmitter {
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);
}

arrayDeclarationContext(array: Model): Context {
return this.#declarationContext(array);
}

interfaceDeclarationContext(iface: Interface): Context {
return this.#declarationContext(iface);
}

operationDeclarationContext(operation: Operation): Context {
return this.#declarationContext(operation);
}

#declarationContext(decl: TypeSpecDeclaration) {
declarationContext(decl: TypeSpecDeclaration) {
const name = this.emitter.emitDeclarationName(decl);
const outputFile = this.emitter.createSourceFile(`${name}.ts`);

Expand Down Expand Up @@ -422,77 +389,11 @@ describe("emitter-framework: typescript emitter", () => {
});

it("emits to namespaces", async () => {
const host = await getHostForTypeSpecFile(testCode);

class NamespacedEmitter extends SingleFileTypescriptEmitter {
private nsByName: Map<string, Scope<string>> = new Map();

modelDeclarationContext(model: Model): Context {
const name = this.emitter.emitDeclarationName(model);
if (!name) return {};
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,
};
}

async sourceFile(
sourceFile: SourceFile<string>
): Promise<EmittedSourceFile> {
const emittedSourceFile = await super.sourceFile(sourceFile);
emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
emittedSourceFile.contents = await 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, {
emitterOutputDir: host.program.compilerOptions.outputDir!,
options: {},
} as any);
emitter.emitProgram();
await emitter.writeOutput();
const contents = (await host.compilerHost.readFile("tsp-output/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/);
const contents = await emitTypeSpecToTs(testCode);
assert.match(contents, /namespace Root/);
assert.match(contents, /namespace Operations/);
assert.match(contents, /namespace WrappedModels/);
assert.match(contents, /Operations\.SomeOp/);
});

it("handles circular references", async () => {
Expand Down

0 comments on commit ec989ea

Please sign in to comment.