From 33bf5cd76e8bfa5499ee542fcd2b0513c46167f3 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 22 Jun 2023 16:12:25 -0500 Subject: [PATCH] onAugment + hover HTTP route proof-of-concept --- docs/standard-library/built-in-decorators.md | 19 +++++++ packages/compiler/lib/decorators.tsp | 6 ++ packages/compiler/src/core/binder.ts | 16 ++++-- packages/compiler/src/core/program.ts | 60 ++++++++++++++------ packages/compiler/src/core/types.ts | 2 +- packages/compiler/src/lib/decorators.ts | 14 +++++ packages/compiler/src/server/type-details.ts | 5 +- packages/http/src/augment.ts | 19 +++++++ packages/http/src/index.ts | 1 + 9 files changed, 116 insertions(+), 26 deletions(-) create mode 100644 packages/http/src/augment.ts diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md index 2d4b0f02fa..1d5f6bc44a 100644 --- a/docs/standard-library/built-in-decorators.md +++ b/docs/standard-library/built-in-decorators.md @@ -211,6 +211,25 @@ nextLink: string; ``` +### `@ideDoc` {#@ideDoc} + +Attach additional documentation to be shown in the IDE + +```typespec +@ideDoc(doc: valueof string) +``` + +#### Target + +`(intrinsic) unknown` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| doc | `valueof scalar string` | Documentation string | + + + ### `@inspectType` {#@inspectType} A debugging decorator used to inspect a type. diff --git a/packages/compiler/lib/decorators.tsp b/packages/compiler/lib/decorators.tsp index 260c260535..4f860296f4 100644 --- a/packages/compiler/lib/decorators.tsp +++ b/packages/compiler/lib/decorators.tsp @@ -29,6 +29,12 @@ extern dec summary(target: unknown, summary: valueof string); */ extern dec doc(target: unknown, doc: valueof string, formatArgs?: {}); +/** + * Attach additional documentation to be shown in the IDE + * @param doc Documentation string + */ +extern dec ideDoc(target: unknown, doc: valueof string); + /** * Mark this type as deprecated * @param message Deprecation message. diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index e51b4a1345..ad0131109f 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -136,12 +136,16 @@ export function createBinder(program: Program): Binder { if (isFunctionName(key)) { name = getFunctionName(key); kind = "decorator"; - if (name === "onValidate") { - program.onValidate(member as any); - continue; - } else if (name === "onEmit") { - // nothing to do here this is loaded as emitter. - continue; + switch (name) { + case "onValidate": + program.onValidate(member as any); + continue; + case "onAugment": + program.onAugment(member as any); + continue; + case "onEmit": + // nothing to do here this is loaded as emitter. + continue; } } else { name = key; diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 856f21adea..db960780a7 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -25,6 +25,7 @@ import { getDirectoryPath, joinPaths, resolvePath } from "./path-utils.js"; import { createProjector } from "./projector.js"; import { CompilerHost, + DecoratorContext, Diagnostic, DiagnosticTarget, Directive, @@ -88,6 +89,7 @@ export interface Program { readonly diagnostics: readonly Diagnostic[]; loadTypeSpecScript(typespecScript: SourceFile): Promise; onValidate(cb: (program: Program) => void | Promise): void; + onAugment(cb: (program: Program) => void | Promise): void; getOption(key: string): string | undefined; stateSet(key: symbol): Set; stateSets: Map; @@ -258,7 +260,8 @@ export async function compile( options: CompilerOptions = {}, oldProgram?: Program // NOTE: deliberately separate from options to avoid memory leak by chaining all old programs together. ): Promise { - const validateCbs: any = []; + const validateCbs: any[] = []; + const augmentCbs: any[] = []; const stateMaps = new Map(); const stateSets = new Map(); const diagnostics: Diagnostic[] = []; @@ -299,6 +302,9 @@ export async function compile( onValidate(cb) { validateCbs.push(cb); }, + onAugment(cb) { + augmentCbs.push(cb); + }, getGlobalNamespaceType, resolveTypeReference, getSourceFileLocationContext, @@ -361,23 +367,16 @@ export async function compile( if (program.hasError()) { return program; } - for (const cb of validateCbs) { - try { - await cb(program); - } catch (error: any) { - if (options.designTimeBuild) { - program.reportDiagnostic( - createDiagnostic({ - code: "on-validate-fail", - format: { error: error.stack }, - target: NoTarget, - }) - ); - } else { - throw error; - } - } - } + + const onAugmentDecoratorContext: DecoratorContext = { + program, + decoratorTarget: NoTarget, + call: (decorator, target, ...args) => decorator(onAugmentDecoratorContext, target, ...args), + getArgumentTarget: () => undefined, + }; + + await runCallbacks(augmentCbs, program, options, onAugmentDecoratorContext); + await runCallbacks(validateCbs, program, options); for (const [requiredImport, emitterName] of requireImports) { if (!loadedLibraries.has(requiredImport)) { @@ -1146,6 +1145,31 @@ export async function compile( } } +async function runCallbacks( + callbacks: any[], + program: Program, + options: CompilerOptions, + arg: Program | DecoratorContext = program +) { + for (const cb of callbacks) { + try { + await cb(arg); + } catch (error: any) { + if (options.designTimeBuild) { + program.reportDiagnostic( + createDiagnostic({ + code: "on-validate-fail", + format: { error: error.stack }, + target: NoTarget, + }) + ); + } else { + throw error; + } + } + } +} + export function createStateAccessors( stateMaps: Map, stateSets: Map, diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 6beba0c0ef..f476eb4db9 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -2016,7 +2016,7 @@ export interface DecoratorContext { /** * Point to the decorator target */ - decoratorTarget: DiagnosticTarget; + decoratorTarget: DiagnosticTarget | typeof NoTarget; /** * Function that can be used to retrieve the target for a parameter at the given index. diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index c525ebee42..3adf7ac288 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -106,6 +106,20 @@ export function $doc(context: DecoratorContext, target: Type, text: string, sour setDocData(context.program, target, { value: text, source: "@doc" }); } +const ideDocsKey = createStateSymbol("ideDocs"); +export function $ideDoc(context: DecoratorContext, target: Type, text: string) { + const ideDocs: string[] = context.program.stateMap(ideDocsKey).get(target); + if (ideDocs) { + ideDocs.push(text); + } else { + context.program.stateMap(ideDocsKey).set(target, [text]); + } +} + +export function getIdeDocs(program: Program, target: Type): string[] { + return program.stateMap(ideDocsKey).get(target) ?? []; +} + /** * @internal to be used to set the `@doc` from doc comment. */ diff --git a/packages/compiler/src/server/type-details.ts b/packages/compiler/src/server/type-details.ts index 91315186a8..b492a5f0bd 100644 --- a/packages/compiler/src/server/type-details.ts +++ b/packages/compiler/src/server/type-details.ts @@ -8,7 +8,7 @@ import { TemplateDeclarationNode, Type, } from "../core/index.js"; -import { getDocData } from "../lib/decorators.js"; +import { getDocData, getIdeDocs } from "../lib/decorators.js"; import { getSymbolSignature } from "./type-signature.js"; /** @@ -69,6 +69,9 @@ function getSymbolDocumentation(program: Program, symbol: Sym) { docs.push(apiDocs.value); } + // Add @ideDoc(...) docs + docs.push(...getIdeDocs(program, type)); + return docs.join("\n\n"); } diff --git a/packages/http/src/augment.ts b/packages/http/src/augment.ts new file mode 100644 index 0000000000..8e254283ac --- /dev/null +++ b/packages/http/src/augment.ts @@ -0,0 +1,19 @@ +import { $ideDoc, DecoratorContext, ignoreDiagnostics } from "@typespec/compiler"; +import { getAllHttpServices } from "./operations.js"; + +export function $onAugment(context: DecoratorContext) { + if (!context.program.compilerOptions.designTimeBuild) { + // we only add IDE docs, so don't do this in non-IDE builds + return; + } + const services = ignoreDiagnostics(getAllHttpServices(context.program)); + for (const service of services) { + for (const operation of service.operations) { + context.call( + $ideDoc, + operation.operation, + `${operation.verb.toUpperCase()} ${operation.path}` + ); + } + } +} diff --git a/packages/http/src/index.ts b/packages/http/src/index.ts index 518b109b31..dcc4f777ac 100644 --- a/packages/http/src/index.ts +++ b/packages/http/src/index.ts @@ -1,5 +1,6 @@ export const namespace = "TypeSpec.Http"; +export * from "./augment.js"; export * from "./content-types.js"; export * from "./decorators.js"; export * from "./metadata.js";