From 3201dd58dbcc6367012a4c12551dac8193fc7f8e Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 10 Feb 2023 08:31:23 -0800 Subject: [PATCH] [Cadl APIView] Version 0.3.5 (#5378) * Updates for cadl-apiview 0.3.5. * Add and update tests. * Final fixes. --- .../emitters/cadl-apiview/CHANGELOG.md | 9 + tools/apiview/emitters/cadl-apiview/README.md | 29 ++-- .../emitters/cadl-apiview/package.json | 19 +- .../emitters/cadl-apiview/src/apiview.ts | 30 +++- .../emitters/cadl-apiview/src/emitter.ts | 163 +++++++++++------- .../apiview/emitters/cadl-apiview/src/lib.ts | 24 ++- .../emitters/cadl-apiview/src/version.ts | 2 +- .../cadl-apiview/test/apiview-options.test.ts | 114 ++++++++++++ .../cadl-apiview/test/apiview.test.ts | 59 +------ .../emitters/cadl-apiview/test/test-host.ts | 82 +++++++++ 10 files changed, 378 insertions(+), 153 deletions(-) create mode 100644 tools/apiview/emitters/cadl-apiview/test/apiview-options.test.ts diff --git a/tools/apiview/emitters/cadl-apiview/CHANGELOG.md b/tools/apiview/emitters/cadl-apiview/CHANGELOG.md index 3dc34431b0e..6b12380fa64 100644 --- a/tools/apiview/emitters/cadl-apiview/CHANGELOG.md +++ b/tools/apiview/emitters/cadl-apiview/CHANGELOG.md @@ -1,5 +1,14 @@ # Release History +## Version 0.3.5 (Unreleased) +Support latest release of Cadl compiler. +**BREAKING CHANGE**: Removed the `--namespace` emitter option. +Added the `--service` emitter option to support filtering output for multi-service specs. +Emitter options `--output-file` and `--version` cannot be used with multi-service specs unless the + `--service` option is provided. +Added the `--include-global-namespace` option to permit including the global namespace in the token file. +Fixed issue where namespaces that are not proper subnamespaces may be included in the token file. + ## Version 0.3.4 (01-13-2023) Support latest release of Cadl compiler. diff --git a/tools/apiview/emitters/cadl-apiview/README.md b/tools/apiview/emitters/cadl-apiview/README.md index 8c5d3ceedc3..7157d0ae539 100644 --- a/tools/apiview/emitters/cadl-apiview/README.md +++ b/tools/apiview/emitters/cadl-apiview/README.md @@ -68,26 +68,35 @@ or via the command line with --option "@azure-tools/cadl-apiview.output-file=my-custom-apiview.json" ``` -### `output-file` +### `emitter-output-dir` + +Configure the name of the output directory. Default is `cadl-output/@azure-tools/cadl-apiview`. -Configure the name of the output JSON token file relative to the `output-dir`. +### `include-global-namespace` -### `output-dir` +Normally, APIView will filter all namespaces and only output those in the service namespace and any +subnamespaces. This is to filter out types that come from the Cadl compiler and supporting libraries. +This setting, if `true`, tells APIView to output the contents of the global (empty) namespace, which +would normally be excluded. -Configure the name of the output directory. Default is `cadl-output/cadl-apiview`. +### `service` -### `namespace` +Filter output to a single service definition. If omitted, all service defintions will be +output as separate APIView token files. + +### `output-file` -For Cadl specs, the namespace should be automatically resolved as the service namespace. If -that doesn't work, or for libraries (which have no service namespace) this option should be -specified to filter the global namespace. Any subnamespaces of the provided namespace will -also be emitted. +Configure the name of the output JSON token file relative to the `output-dir`. For multi-service +specs, this option cannot be supplied unless the `service` option is also set. If outputting +all services in a multi-service spec, the output filename will be the service root namespace with the +`-apiview.json` suffix. Otherwise, the default is `apiview.json`. ### `version` For multi-versioned Cadl specs, this parameter is used to control which version to emit. This is not required for single-version specs. For multi-versioned specs, the unprojected Cadl will -be rendered if this is not supplied. +be rendered if this is not supplied. For multi-service specs, this option cannot be supplied +unless the `service` option is also set. ## See also diff --git a/tools/apiview/emitters/cadl-apiview/package.json b/tools/apiview/emitters/cadl-apiview/package.json index 4116c80d373..0950984ac6e 100644 --- a/tools/apiview/emitters/cadl-apiview/package.json +++ b/tools/apiview/emitters/cadl-apiview/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/cadl-apiview", - "version": "0.3.4", + "version": "0.3.5", "author": "Microsoft Corporation", "description": "Cadl library for emitting APIView token files from Cadl specifications", "homepage": "https://github.com/Azure/azure-sdk-tools", @@ -54,16 +54,17 @@ "!dist/test/**" ], "dependencies": { - "@azure-tools/cadl-azure-core": "~0.25.0", - "@azure-tools/cadl-dpg": "~0.25.0", - "@cadl-lang/compiler": "~0.39.0", - "@cadl-lang/rest": "~0.39.0", - "@cadl-lang/versioning": "~0.39.0" + "@azure-tools/cadl-azure-core": "0.26.0", + "@azure-tools/cadl-autorest": "0.26.0", + "@azure-tools/cadl-dpg": "latest", + "@cadl-lang/compiler": "0.40.0", + "@cadl-lang/rest": "latest", + "@cadl-lang/versioning": "latest" }, "devDependencies": { - "@cadl-lang/eslint-plugin": "~0.39.0", - "@cadl-lang/library-linter": "~0.39.0", - "@cadl-lang/prettier-plugin-cadl": "^0.39.0", + "@cadl-lang/eslint-plugin": "~0.40.0", + "@cadl-lang/library-linter": "~0.40.0", + "@cadl-lang/prettier-plugin-cadl": "^0.40.0", "@cadl-lang/eslint-config-cadl": "~0.4.1", "@types/mocha": "~9.1.0", "@types/node": "~16.0.3", diff --git a/tools/apiview/emitters/cadl-apiview/src/apiview.ts b/tools/apiview/emitters/cadl-apiview/src/apiview.ts index 2cbc9dfcdbe..a103706b2bc 100644 --- a/tools/apiview/emitters/cadl-apiview/src/apiview.ts +++ b/tools/apiview/emitters/cadl-apiview/src/apiview.ts @@ -8,6 +8,7 @@ import { EnumMemberNode, EnumSpreadMemberNode, EnumStatementNode, + getNamespaceFullName, IdentifierNode, InterfaceStatementNode, IntersectionExpressionNode, @@ -90,11 +91,13 @@ export class ApiView { indentSize: number = 2; namespaceStack = new NamespaceStack(); typeDeclarations = new Set(); + includeGlobalNamespace: boolean; - constructor(name: string, packageName: string, versionString?: string) { + constructor(name: string, packageName: string, versionString?: string, includeGlobalNamespace?: boolean) { this.name = name; this.packageName = packageName; this.versionString = versionString ?? ""; + this.includeGlobalNamespace = includeGlobalNamespace ?? false; this.emitHeader(); } @@ -309,24 +312,39 @@ export class ApiView { this.navigationItems.push(item); } + shouldEmitNamespace(name: string): boolean { + if (name === "" && this.includeGlobalNamespace) { + return true; + } + if (name === this.packageName) { + return true; + } + if (!name.startsWith(this.packageName)) { + return false; + } + const suffix = name.substring(this.packageName.length); + return suffix.startsWith("."); + } + emit(program: Program) { let allNamespaces = new Map(); // collect namespaces in program navigateProgram(program, { namespace(obj) { - const name = program.checker.getNamespaceString(obj); + const name = getNamespaceFullName(obj); allNamespaces.set(name, obj); }, }); allNamespaces = new Map([...allNamespaces].sort()); - // Skip namespaces which are outside the root namespace. for (const [name, ns] of allNamespaces.entries()) { - if (!name.startsWith(this.packageName)) { + if (!this.shouldEmitNamespace(name)) { continue; } - const nsModel = new NamespaceModel(name, ns, program); + // use a fake name to make the global namespace clear + const namespaceName = name == "" ? "::GLOBAL::" : name; + const nsModel = new NamespaceModel(namespaceName, ns, program); if (nsModel.shouldEmit()) { this.tokenizeNamespaceModel(nsModel); this.buildNavigation(nsModel); @@ -833,7 +851,7 @@ export class ApiView { this.blankLines(1); } this.endGroup(); - this.newline(); + this.blankLines(1); this.namespaceStack.pop(); } diff --git a/tools/apiview/emitters/cadl-apiview/src/emitter.ts b/tools/apiview/emitters/cadl-apiview/src/emitter.ts index 2e4ccea7d64..09e0f7322b9 100644 --- a/tools/apiview/emitters/cadl-apiview/src/emitter.ts +++ b/tools/apiview/emitters/cadl-apiview/src/emitter.ts @@ -3,30 +3,27 @@ import { EmitContext, emitFile, - getServiceNamespace, - getServiceTitle, - getServiceVersion, + getNamespaceFullName, + listServices, Namespace, NoTarget, Program, ProjectionApplication, projectProgram, resolvePath, + Service, } from "@cadl-lang/compiler"; import { buildVersionProjections, getVersion } from "@cadl-lang/versioning"; import path from "path"; import { ApiView } from "./apiview.js"; import { ApiViewEmitterOptions, reportDiagnostic } from "./lib.js"; -const defaultOptions = { - "output-file": "apiview.json", -} as const; - export interface ResolvedApiViewEmitterOptions { - emitterOutputDir?: string; - outputPath: string; - namespace?: string; + emitterOutputDir: string; + outputFile?: string; + service?: string; version?: string; + includeGlobalNamespace: boolean; } export async function $onEmit(context: EmitContext) { @@ -36,35 +33,28 @@ export async function $onEmit(context: EmitContext) { } export function resolveOptions(context: EmitContext): ResolvedApiViewEmitterOptions { - const resolvedOptions = { ...defaultOptions, ...context.options }; + const resolvedOptions = { ...context.options }; return { - outputPath: resolvePath( - context.emitterOutputDir, - resolvedOptions["output-file"] - ), - namespace: resolvedOptions["namespace"], + emitterOutputDir: context.emitterOutputDir, + outputFile: resolvedOptions["output-file"], + service: resolvedOptions["service"], version: resolvedOptions["version"], + includeGlobalNamespace: resolvedOptions["include-global-namespace"] ?? false }; } -function resolveServiceVersion(program: Program): string | undefined { - // FIXME: Fix this wonky workaround when getServiceVersion is fixed. - const value = getServiceVersion(program); - return value == "0000-00-00" ? undefined : value; -} - -function resolveNamespaceString(program: Program, namespace: Namespace): string | undefined { +function resolveNamespaceString(namespace: Namespace): string | undefined { // FIXME: Fix this wonky workaround when getNamespaceString is fixed. - const value = program.checker.getNamespaceString(namespace); + const value = getNamespaceFullName(namespace); return value == "" ? undefined : value; } // TODO: Up-level this logic? -function resolveAllowedVersions(program: Program, namespace: Namespace): string[] { +function resolveAllowedVersions(program: Program, service: Service): string[] { const allowed: string[] = []; - const serviceVersion = resolveServiceVersion(program); - const versions = getVersion(program, namespace)?.getVersions(); + const serviceVersion = service.version; + const versions = getVersion(program, service.type)?.getVersions(); if (serviceVersion != undefined && versions != undefined) { throw new Error("Cannot have serviceVersion with multi-API."); } @@ -115,54 +105,97 @@ function resolveProgramForVersion(program: Program, namespace: Namespace, versio } } +/** + * Ensures that single-value options are not used in multi-service specs unless the + * `--service` option is specified. Single-service specs need not pass this option. + */ +function validateMultiServiceOptions(program: Program, services: Service[], options: ResolvedApiViewEmitterOptions) { + for (const [name, val] of [["output-file", options.outputFile], ["version", options.version]]) { + if (val && !options.service && services.length > 1) { + reportDiagnostic(program, { + code: "invalid-option", + target: NoTarget, + format: { + name: name! + } + }) + } + } +} + +/** + * If the `--service` option is provided, ensures the service exists and returns the filtered list. + */ +function applyServiceFilter(program: Program, services: Service[], options: ResolvedApiViewEmitterOptions): Service[] { + if (!options.service) { + return services; + } + const filtered = services.filter( (x) => x.title === options.service); + if (!filtered.length) { + reportDiagnostic(program, { + code: "invalid-service", + target: NoTarget, + format: { + value: options.service + } + }); + } + return filtered; +} + function createApiViewEmitter(program: Program, options: ResolvedApiViewEmitterOptions) { return { emitApiView }; async function emitApiView() { - const serviceNs = getServiceNamespace(program); - if (!serviceNs) { - throw new Error("No namespace found"); - } - const versionString = options.version ?? resolveServiceVersion(program); - const namespaceString = options.namespace ?? resolveNamespaceString(program, serviceNs); - if (namespaceString == undefined) { + let services = listServices(program); + if (!services.length) { reportDiagnostic(program, { - code: "use-namespace-option", + code: "no-services-found", target: NoTarget - }); + }) return; } - const allowedVersions = resolveAllowedVersions(program, serviceNs); - if (versionString) { - if (allowedVersions.filter((version) => version == versionString).length == 0) { - reportDiagnostic(program, { - code: "version-not-found", - target: NoTarget, - format: { - version: versionString, - allowed: allowedVersions.join(" | "), - }, - }) - return; - } + // applies the default "apiview.json" filename if not provided and there's only a single service + if (services.length == 1) { + options.outputFile = options.outputFile ?? "apiview.json" } - // FIXME: Fix this wonky workaround when getServiceTitle is fixed. - let serviceTitle = getServiceTitle(program); - if (serviceTitle == "(title)") { - serviceTitle = namespaceString; - } - const resolvedProgram = resolveProgramForVersion(program, serviceNs, versionString); - const apiview = new ApiView(serviceTitle, namespaceString, versionString); - apiview.emit(resolvedProgram); - apiview.resolveMissingTypeReferences(); + validateMultiServiceOptions(program, services, options); + services = applyServiceFilter(program, services, options); + + for (const service of services) { + const versionString = options.version ?? service.version; + const namespaceString = resolveNamespaceString(service.type) ?? "Unknown" + const serviceTitle = service.title ? service.title : namespaceString; + const allowedVersions = resolveAllowedVersions(program, service); + if (versionString) { + if (allowedVersions.filter((version) => version == versionString).length == 0) { + reportDiagnostic(program, { + code: "version-not-found", + target: NoTarget, + format: { + version: versionString, + serviceTitle: serviceTitle, + allowed: allowedVersions.join(" | "), + }, + }) + return; + } + } + const resolvedProgram = resolveProgramForVersion(program, service.type, versionString); + const apiview = new ApiView(serviceTitle, namespaceString, versionString, options.includeGlobalNamespace); + apiview.emit(resolvedProgram); + apiview.resolveMissingTypeReferences(); - if (!program.compilerOptions.noEmit && !program.hasError()) { - const outputFolder = path.dirname(options.outputPath); - await program.host.mkdirp(outputFolder); - await emitFile(program, { - path: options.outputPath, - content: JSON.stringify(apiview.asApiViewDocument()) + "\n" - }); + if (!program.compilerOptions.noEmit && !program.hasError()) { + const outputFolder = path.dirname(options.emitterOutputDir); + await program.host.mkdirp(outputFolder); + const outputFile = options.outputFile ?? `${namespaceString}-apiview.json`; + const outputPath = resolvePath(outputFolder, outputFile); + await emitFile(program, { + path: outputPath, + content: JSON.stringify(apiview.asApiViewDocument()) + "\n" + }); + } } } } diff --git a/tools/apiview/emitters/cadl-apiview/src/lib.ts b/tools/apiview/emitters/cadl-apiview/src/lib.ts index d7822b61c20..a2e613b90e3 100644 --- a/tools/apiview/emitters/cadl-apiview/src/lib.ts +++ b/tools/apiview/emitters/cadl-apiview/src/lib.ts @@ -2,8 +2,9 @@ import { createCadlLibrary, JSONSchemaType, paramMessage } from "@cadl-lang/comp export interface ApiViewEmitterOptions { "output-file"?: string; - "namespace"?: string; + "service"?: string; "version"?: string; + "include-global-namespace"?: boolean } const ApiViewEmitterOptionsSchema: JSONSchemaType = { @@ -11,8 +12,9 @@ const ApiViewEmitterOptionsSchema: JSONSchemaType = { additionalProperties: false, properties: { "output-file": { type: "string", nullable: true }, - "namespace": { type: "string", nullable: true }, + "service": { type: "string", nullable: true }, "version": {type: "string", nullable: true }, + "include-global-namespace": {type: "boolean", nullable: true} }, required: [], }; @@ -21,16 +23,28 @@ const ApiViewEmitterOptionsSchema: JSONSchemaType = { export const $lib = createCadlLibrary({ name: "@azure-tools/cadl-apiview", diagnostics: { - "use-namespace-option": { + "no-services-found": { severity: "error", messages: { - default: "Unable to resolve namespace. Please supply `--option \"@azure-tools/cadl-apiview.namespace={value}\"`.", + default: "No services found. Ensure there is a namespace in the spec annotated with the `@service` decorator." + } + }, + "invalid-service": { + severity: "error", + messages: { + default: paramMessage`Service "${"value"}" was not found. Please check for typos.`, + } + }, + "invalid-option": { + severity: "error", + messages: { + default: paramMessage`Option "--${"name"}" cannot be used with multi-service specs unless "--service" is also supplied.`, } }, "version-not-found": { severity: "error", messages: { - default: paramMessage`Version "${"version"}" not found. Allowed values: ${"allowed"}.`, + default: paramMessage`Version "${"version"}" not found for service "${"serviceName"}". Allowed values: ${"allowed"}.`, } }, }, diff --git a/tools/apiview/emitters/cadl-apiview/src/version.ts b/tools/apiview/emitters/cadl-apiview/src/version.ts index 2531369b2bd..633c84a9da6 100644 --- a/tools/apiview/emitters/cadl-apiview/src/version.ts +++ b/tools/apiview/emitters/cadl-apiview/src/version.ts @@ -1 +1 @@ -export const LIB_VERSION = "0.3.4"; +export const LIB_VERSION = "0.3.5"; diff --git a/tools/apiview/emitters/cadl-apiview/test/apiview-options.test.ts b/tools/apiview/emitters/cadl-apiview/test/apiview-options.test.ts new file mode 100644 index 00000000000..29ea0a74afb --- /dev/null +++ b/tools/apiview/emitters/cadl-apiview/test/apiview-options.test.ts @@ -0,0 +1,114 @@ +import { Diagnostic, logDiagnostics, resolvePath } from "@cadl-lang/compiler"; +import { expectDiagnosticEmpty, expectDiagnostics } from "@cadl-lang/compiler/testing"; +import { strictEqual } from "assert"; +import { apiViewFor, apiViewText, compare, createApiViewTestRunner, diagnosticsFor } from "./test-host.js"; + +describe("apiview-options: tests", () => { + + it("omits namespaces that aren't proper subnamespaces", async () => { + const input = ` + @Cadl.service( { title: "Test", version: "1" } ) + namespace Azure.Test { + model Foo {}; + } + + namespace Azure.Test.Sub { + model SubFoo {}; + }; + + namespace Azure.TestBad { + model BadFoo {}; + }; + `; + const expect = ` + namespace Azure.Test { + model Foo {} + } + + namespace Azure.Test.Sub { + model SubFoo {} + } + ` + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 9); + }); + + it("outputs the global namespace when --include-global-namespace is set", async () => { + const input = ` + model SomeGlobal {}; + + @Cadl.service( { title: "Test", version: "1" } ) + namespace Azure.Test { + model Foo {}; + } + `; + const expect = ` + namespace ::GLOBAL:: { + model SomeGlobal {} + } + + @Cadl.service( + { + title: "Test"; + version: "1"; + } + ) + namespace Azure.Test { + model Foo {} + } + ` + const apiview = await apiViewFor(input, { + "include-global-namespace": true + }); + const actual = apiViewText(apiview); + compare(expect, actual, 1); + }); + + it("emits error if multi-service package tries to specify version", async () => { + const input = ` + @Cadl.service( { title: "Test", version: "1" } ) + namespace Azure.Test { + model Foo {}; + } + + @Cadl.service( { title: "OtherTest", version: "1" } ) + namespace Azure.OtherTest { + model Foo {}; + } + ` + const diagnostics = await diagnosticsFor(input, {"version": "1"}); + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/cadl-apiview/invalid-option", + message: `Option "--output-file" cannot be used with multi-service specs unless "--service" is also supplied.` + }, + { + code: "@azure-tools/cadl-apiview/invalid-option", + message: `Option "--version" cannot be used with multi-service specs unless "--service" is also supplied.` + } + ]); + }); + + it("allows options if multi-service package specifies --service", async () => { + const input = ` + @Cadl.service( { title: "Test", version: "1" } ) + namespace Azure.Test { + model Foo {}; + } + + @Cadl.service( { title: "OtherTest", version: "1" } ) + namespace Azure.OtherTest { + model Foo {}; + } + `; + const expect = ` + namespace Azure.OtherTest { + model Foo {} + } + `; + const apiview = await apiViewFor(input, {"version": "1", "service": "OtherTest"}); + const actual = apiViewText(apiview); + compare(expect, actual, 9); + }); +}); diff --git a/tools/apiview/emitters/cadl-apiview/test/apiview.test.ts b/tools/apiview/emitters/cadl-apiview/test/apiview.test.ts index c33f80a916d..97f9689bb48 100644 --- a/tools/apiview/emitters/cadl-apiview/test/apiview.test.ts +++ b/tools/apiview/emitters/cadl-apiview/test/apiview.test.ts @@ -1,64 +1,8 @@ -import { Diagnostic, logDiagnostics, resolvePath } from "@cadl-lang/compiler"; -import { expectDiagnosticEmpty } from "@cadl-lang/compiler/testing"; import assert, { fail, strictEqual } from "assert"; import { ApiViewDocument, ApiViewTokenKind } from "../src/apiview.js"; -import { ApiViewEmitterOptions } from "../src/lib.js"; -import { createApiViewTestRunner } from "./test-host.js"; +import { apiViewFor, apiViewText, compare } from "./test-host.js"; describe("apiview: tests", () => { - async function apiViewFor(code: string, options: ApiViewEmitterOptions): Promise { - const runner = await createApiViewTestRunner({withVersioning: true}); - const outPath = resolvePath("/apiview.json"); - await runner.compile(code, { - noEmit: false, - emitters: { "@azure-tools/cadl-apiview": { ...options, "output-file": outPath } }, - miscOptions: { "disable-linter": true }, - }); - - const jsonText = runner.fs.get(outPath)!; - const apiview = JSON.parse(jsonText) as ApiViewDocument; - return apiview; - } - - function apiViewText(apiview: ApiViewDocument): string[] { - const vals = new Array; - for (const token of apiview.Tokens) { - switch (token.Kind) { - case ApiViewTokenKind.Newline: - vals.push("\n"); - break; - default: - if (token.Value != undefined) { - vals.push(token.Value); - } - break; - } - } - return vals.join("").split("\n"); - } - - /** Compares an expected string to a subset of the actual output. */ - function compare(expect: string, lines: string[], offset: number) { - // split the input into lines and ignore leading or trailing empty lines. - let expectedLines = expect.split("\n"); - if (expectedLines[0].trim() == '') { - expectedLines = expectedLines.slice(1); - } - if (expectedLines[expectedLines.length - 1].trim() == '') { - expectedLines = expectedLines.slice(0, -1); - } - // remove any leading indentation - const indent = expectedLines[0].length - expectedLines[0].trimStart().length; - for (let x = 0; x < expectedLines.length; x++) { - expectedLines[x] = expectedLines[x].substring(indent); - } - const checkLines = lines.slice(offset, offset + expectedLines.length); - strictEqual(expectedLines.length, checkLines.length); - for (let x = 0; x < checkLines.length; x++) { - strictEqual(expectedLines[x], checkLines[x], `Actual differed from expected at line #${x + 1}\nACTUAL: '${checkLines[x]}'\nEXPECTED: '${expectedLines[x]}'`); - } - } - /** Validates that there are no repeat defintion IDs and that each line has only one definition ID. */ function validateDefinitionIds(apiview: ApiViewDocument) { const definitionIds = new Set(); @@ -188,6 +132,7 @@ describe("apiview: tests", () => { } alias Creature = Animal + } `; const apiview = await apiViewFor(input, {}); const actual = apiViewText(apiview); diff --git a/tools/apiview/emitters/cadl-apiview/test/test-host.ts b/tools/apiview/emitters/cadl-apiview/test/test-host.ts index faaec360484..6f030ba197b 100644 --- a/tools/apiview/emitters/cadl-apiview/test/test-host.ts +++ b/tools/apiview/emitters/cadl-apiview/test/test-host.ts @@ -4,6 +4,10 @@ import { VersioningTestLibrary } from "@cadl-lang/versioning/testing"; import { AzureCoreTestLibrary } from "@azure-tools/cadl-azure-core/testing"; import { ApiViewTestLibrary } from "../src/testing/index.js"; import "@azure-tools/cadl-apiview"; +import { ApiViewEmitterOptions } from "../src/lib.js"; +import { ApiViewDocument, ApiViewTokenKind } from "../src/apiview.js"; +import { Diagnostic, resolvePath } from "@cadl-lang/compiler"; +import { strictEqual } from "assert"; export async function createApiViewTestHost() { return createTestHost({ @@ -29,3 +33,81 @@ export async function createApiViewTestRunner({ } }); } + +export async function diagnosticsFor(code: string, options: ApiViewEmitterOptions): Promise { + const runner = await createApiViewTestRunner({withVersioning: true}); + const outPath = resolvePath("/apiview.json"); + const diagnostics = await runner.diagnose(code, { + noEmit: false, + emitters: { "@azure-tools/cadl-apiview": { ...options, "output-file": outPath } }, + miscOptions: { "disable-linter": true }, + }); + return diagnostics; +} + +export async function apiViewFor(code: string, options: ApiViewEmitterOptions): Promise { + const runner = await createApiViewTestRunner({withVersioning: true}); + const outPath = resolvePath("/apiview.json"); + await runner.compile(code, { + noEmit: false, + emitters: { "@azure-tools/cadl-apiview": { ...options, "output-file": outPath } }, + miscOptions: { "disable-linter": true }, + }); + + const jsonText = runner.fs.get(outPath)!; + const apiview = JSON.parse(jsonText) as ApiViewDocument; + return apiview; +} + +export function apiViewText(apiview: ApiViewDocument): string[] { + const vals = new Array; + for (const token of apiview.Tokens) { + switch (token.Kind) { + case ApiViewTokenKind.Newline: + vals.push("\n"); + break; + default: + if (token.Value != undefined) { + vals.push(token.Value); + } + break; + } + } + return vals.join("").split("\n"); +} + +function getIndex(lines: string[]): number { + for (const line of lines) { + if (line.trim() !== "") { + return line.length - line.trimStart().length; + } + } + return 0; +} + +/** Eliminates leading indentation and blank links that can mess with comparisons */ +function trimLines(lines: string[]): string[] { + const trimmed: string[] = []; + const indent = getIndex(lines); + for (const line of lines) { + if (line.trim() == '') { + // skip blank lines + continue; + } else { + // remove any leading indentation + trimmed.push(line.substring(indent)); + } + } + return trimmed; +} + +/** Compares an expected string to a subset of the actual output. */ +export function compare(expect: string, lines: string[], offset: number) { + // split the input into lines and ignore leading or trailing empty lines. + const expectedLines = trimLines(expect.split("\n")); + const checkLines = trimLines(lines.slice(offset)); + strictEqual(expectedLines.length, checkLines.length); + for (let x = 0; x < checkLines.length; x++) { + strictEqual(expectedLines[x], checkLines[x], `Actual differed from expected at line #${x + 1}\nACTUAL: '${checkLines[x]}'\nEXPECTED: '${expectedLines[x]}'`); + } +}