diff --git a/tools/apiview/emitters/typespec-apiview/CHANGELOG.md b/tools/apiview/emitters/typespec-apiview/CHANGELOG.md index f19e8ad0f5c..7c6b59b1b0e 100644 --- a/tools/apiview/emitters/typespec-apiview/CHANGELOG.md +++ b/tools/apiview/emitters/typespec-apiview/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## Version 0.4.7 (03-22-2024) +Support TypeSpec string templates. +Fix display issue with templated aliases. +Ensure alias statements end with semicolon. + ## Version 0.4.6 (03-08-2024) Support CrossLanguagePackageId. diff --git a/tools/apiview/emitters/typespec-apiview/package-lock.json b/tools/apiview/emitters/typespec-apiview/package-lock.json index 33c6c40eb70..6e29b5300ca 100644 --- a/tools/apiview/emitters/typespec-apiview/package-lock.json +++ b/tools/apiview/emitters/typespec-apiview/package-lock.json @@ -62,25 +62,6 @@ "@typespec/rest": "~0.53.0" } }, - "node_modules/@azure-tools/typespec-client-generator-core": { - "version": "0.39.1", - "resolved": "https://registry.npmjs.org/@azure-tools/typespec-client-generator-core/-/typespec-client-generator-core-0.39.1.tgz", - "integrity": "sha512-EV3N6IN1i/hXGqYKNfXx6+2QAyZnG4IpC9RUk6fqwSQDWX7HtMcfdXqlOaK3Rz2H6BUAc9OnH+Trq/uJCl/RgA==", - "dev": true, - "dependencies": { - "change-case": "~5.3.0", - "pluralize": "^8.0.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@typespec/compiler": "~0.53.1", - "@typespec/http": "~0.53.0", - "@typespec/rest": "~0.53.0", - "@typespec/versioning": "~0.53.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -1841,7 +1822,8 @@ "node_modules/change-case": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.3.0.tgz", - "integrity": "sha512-Eykca0fGS/xYlx2fG5NqnGSnsWauhSGiSXYhB1kO6E909GUfo8S54u4UZNS7lMJmgZumZ2SUpWaoLgAcfQRICg==" + "integrity": "sha512-Eykca0fGS/xYlx2fG5NqnGSnsWauhSGiSXYhB1kO6E909GUfo8S54u4UZNS7lMJmgZumZ2SUpWaoLgAcfQRICg==", + "peer": true }, "node_modules/charenc": { "version": "0.0.2", diff --git a/tools/apiview/emitters/typespec-apiview/package.json b/tools/apiview/emitters/typespec-apiview/package.json index 0d916f01db8..102f28be4ea 100644 --- a/tools/apiview/emitters/typespec-apiview/package.json +++ b/tools/apiview/emitters/typespec-apiview/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/typespec-apiview", - "version": "0.4.6", + "version": "0.4.7", "author": "Microsoft Corporation", "description": "Library for emitting APIView token files from TypeSpec", "homepage": "https://github.com/Azure/azure-sdk-tools", diff --git a/tools/apiview/emitters/typespec-apiview/src/apiview.ts b/tools/apiview/emitters/typespec-apiview/src/apiview.ts index c0da1e5b152..0f5e1a6a1d5 100644 --- a/tools/apiview/emitters/typespec-apiview/src/apiview.ts +++ b/tools/apiview/emitters/typespec-apiview/src/apiview.ts @@ -8,12 +8,12 @@ import { EnumMemberNode, EnumSpreadMemberNode, EnumStatementNode, + Expression, getNamespaceFullName, getSourceLocation, IdentifierNode, InterfaceStatementNode, IntersectionExpressionNode, - listServices, MemberExpressionNode, ModelExpressionNode, ModelPropertyNode, @@ -21,7 +21,6 @@ import { ModelStatementNode, Namespace, navigateProgram, - NoTarget, NumericLiteralNode, OperationSignatureDeclarationNode, OperationSignatureReferenceNode, @@ -29,6 +28,9 @@ import { Program, ScalarStatementNode, StringLiteralNode, + StringTemplateExpressionNode, + StringTemplateHeadNode, + StringTemplateSpanNode, SyntaxKind, TemplateArgumentNode, TemplateParameterDeclarationNode, @@ -43,7 +45,6 @@ import { ApiViewDiagnostic, ApiViewDiagnosticLevel } from "./diagnostic.js"; import { ApiViewNavigation } from "./navigation.js"; import { generateId, NamespaceModel } from "./namespace-model.js"; import { LIB_VERSION } from "./version.js"; -import { reportDiagnostic } from "./lib.js"; const WHITESPACE = " "; @@ -383,6 +384,7 @@ export class ApiView { this.namespaceStack.push(obj.id.sv); this.keyword("alias", false, true); this.typeDeclaration(obj.id.sv, this.namespaceStack.value(), true); + this.tokenizeTemplateParameters(obj.templateParameters); this.punctuation("=", true, true); this.tokenize(obj.value); this.namespaceStack.pop(); @@ -633,10 +635,83 @@ export class ApiView { this.tokenize(obj.argument); } break; + case SyntaxKind.StringTemplateExpression: + obj = node as StringTemplateExpressionNode; + const stringValue = this.buildTemplateString(obj); + const multiLine = stringValue.includes("\n"); + // single line case + if (!multiLine) { + this.stringLiteral(stringValue); + break; + } + // otherwise multiline case + const lines = stringValue.split("\n"); + this.punctuation(`"""`); + this.newline(); + this.indent(); + for (const line of lines) { + this.literal(line); + this.newline(); + } + this.deindent(); + this.punctuation(`"""`); + break; + case SyntaxKind.StringTemplateSpan: + obj = node as StringTemplateSpanNode; + this.punctuation("${", false, false); + this.tokenize(obj.expression); + this.punctuation("}", false, false); + this.tokenize(obj.literal); + break; + case SyntaxKind.StringTemplateHead: + case SyntaxKind.StringTemplateMiddle: + case SyntaxKind.StringTemplateTail: + obj = node as StringTemplateHeadNode; + this.literal(obj.value); + break; + default: + // All Projection* cases should fail here... + throw new Error(`Case "${SyntaxKind[node.kind].toString()}" not implemented`); + } + } + + private buildExpressionString(node: Expression) { + switch (node.kind) { + case SyntaxKind.StringLiteral: + return `"${(node as StringLiteralNode).value}"`; + case SyntaxKind.NumericLiteral: + return (node as NumericLiteralNode).value.toString(); + case SyntaxKind.BooleanLiteral: + return (node as BooleanLiteralNode).value.toString(); + case SyntaxKind.StringTemplateExpression: + return this.buildTemplateString(node as StringTemplateExpressionNode); + case SyntaxKind.VoidKeyword: + return "void"; + case SyntaxKind.NeverKeyword: + return "never"; + case SyntaxKind.TypeReference: + const obj = node as TypeReferenceNode; + switch (obj.target.kind) { + case SyntaxKind.Identifier: + return (obj.target as IdentifierNode).sv; + case SyntaxKind.MemberExpression: + return this.getFullyQualifiedIdentifier(obj.target as MemberExpressionNode); + } + break; default: - // All Projection* cases should fall in here... - throw new Error(`Case "${node.kind.toString()}" not implemented`); + throw new Error(`Unsupported expression kind: ${SyntaxKind[node.kind]}`); + //unsupported ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode | AnyKeywordNode; + } + } + + /** Constructs a single string with template markers. */ + private buildTemplateString(node: StringTemplateExpressionNode): string { + let result = node.head.value; + for (const span of node.spans) { + result += "${" + this.buildExpressionString(span.expression) + "}"; + result += span.literal.value; } + return result; } private tokenizeModelStatement(node: ModelStatementNode) { @@ -891,6 +966,7 @@ export class ApiView { } for (const node of model.aliases.values()) { this.tokenize(node); + this.punctuation(";"); this.blankLines(1); } this.endGroup(); diff --git a/tools/apiview/emitters/typespec-apiview/src/version.ts b/tools/apiview/emitters/typespec-apiview/src/version.ts index a141fe0ee1e..cc8622051e1 100644 --- a/tools/apiview/emitters/typespec-apiview/src/version.ts +++ b/tools/apiview/emitters/typespec-apiview/src/version.ts @@ -1 +1 @@ -export const LIB_VERSION = "0.4.6"; +export const LIB_VERSION = "0.4.7"; diff --git a/tools/apiview/emitters/typespec-apiview/test/apiview.test.ts b/tools/apiview/emitters/typespec-apiview/test/apiview.test.ts index 2f6692057d7..de57ee10723 100644 --- a/tools/apiview/emitters/typespec-apiview/test/apiview.test.ts +++ b/tools/apiview/emitters/typespec-apiview/test/apiview.test.ts @@ -243,7 +243,7 @@ describe("apiview: tests", () => { species: string; } - alias Creature = Animal + alias Creature = Animal; } `; const apiview = await apiViewFor(input, {}); @@ -251,7 +251,33 @@ describe("apiview: tests", () => { compare(expect, actual, 9); validateDefinitionIds(apiview); }); - }); + + it("templated alias", async () => { + const input = ` + @TypeSpec.service( { title: "Test", version: "1" } ) + namespace Azure.Test { + model Animal { + species: string; + } + + alias Template = "Foo \${T} bar"; + } + `; + const expect = ` + namespace Azure.Test { + model Animal { + species: string; + } + + alias Template = "Foo \${T} bar"; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 9); + validateDefinitionIds(apiview); + }); + }); describe("augment decorators", () => { it("simple augment", async () => { @@ -693,4 +719,50 @@ describe("apiview: tests", () => { validateDefinitionIds(apiview); }); }); + + describe("string templates", () => { + it("templates", async () => { + const input = ` + @TypeSpec.service( { title: "Test", version: "1" } ) + namespace Azure.Test { + alias myconst = "foobar"; + model Person { + simple: "Simple \${123} end"; + multiline: """ + Multi + \${123} + \${true} + line + """; + ref: "Ref this alias \${myconst} end"; + template: Template<"custom">; + } + alias Template = "Foo \${T} bar"; + }`; + + const expect = ` + namespace Azure.Test { + model Person { + simple: "Simple \${123} end"; + multiline: """ + Multi + \${123} + \${true} + line + """; + ref: "Ref this alias \${myconst} end"; + template: Template<"custom">; + } + + alias myconst = "foobar"; + + alias Template = "Foo \${T} bar"; + } + `; + const apiview = await apiViewFor(input, {}); + const lines = apiViewText(apiview); + compare(expect, lines, 9); + validateDefinitionIds(apiview); + }); + }); });