Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/diagnostic-docs-openapi3-2026-6-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/openapi3"
---

Provide extended documentation for several diagnostics (`path-query`, `duplicate-header`, `inline-cycle`, `invalid-schema`, `invalid-server-variable`, `union-null`) via co-located markdown files.
7 changes: 7 additions & 0 deletions .chronus/changes/hover-diagnostic-docs-2026-6-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

The language server now surfaces the extended documentation of diagnostics and linter rules when hovering over a reported error. If the diagnostic/rule provides `docs` (inline markdown or a `FileRef`), it is rendered in the hover, together with a link to the generated reference page when a documentation url is available.
20 changes: 20 additions & 0 deletions .chronus/changes/reference-docs-base-url-2026-6-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add a `referenceDocs.baseUrl` field to the library definition passed to `createTypeSpecLibrary`. When set, the compiler auto-generates the `url` of each documented diagnostic and linter rule that does not specify one explicitly, so tooling (CLI and editor) can link to the generated reference pages without hardcoding a URL per rule/diagnostic:

- diagnostics -> `${baseUrl}/diagnostics/<code>`
- linter rules -> `${baseUrl}/rules/<name>`

```ts
export const $lib = createTypeSpecLibrary({
name: "@typespec/my-lib",
referenceDocs: { baseUrl: "https://typespec.io/docs/libraries/my-lib/reference" },
diagnostics: {
/* ... */
},
});
```
19 changes: 19 additions & 0 deletions .chronus/changes/rule-diagnostic-docs-compiler-2026-6-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add a `docs` field to linter rule and diagnostic definitions to provide extended reference documentation. The value can be an inline markdown string or a `FileRef` created with `fileRef.fromPackageRoot("src/rules/my-rule.md")`, which is read lazily by tooling so it stays safe to bundle for the browser.

```ts
export const myRule = createRule({
name: "my-rule",
severity: "warning",
description: "Short description.",
docs: fileRef.fromPackageRoot("src/rules/my-rule.md"),
messages: {
/* ... */
},
});
```
7 changes: 7 additions & 0 deletions .chronus/changes/rule-diagnostic-docs-tspd-2026-6-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/tspd"
---

`tspd doc` now generates a documentation page per linter rule (`reference/rules/<name>.md`) and per diagnostic (`reference/diagnostics/<code>.md`), sourced from the `docs` field on the rule and diagnostic definitions. A `documentation-missing` warning is reported for any linter rule or diagnostic that does not provide documentation.
7 changes: 7 additions & 0 deletions .chronus/changes/rule-docs-http-2026-6-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http"
---

Provide extended documentation for the `op-reference-container-route` linter rule via a co-located markdown file.
4 changes: 4 additions & 0 deletions packages/compiler/src/core/diagnostic-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
export function createDiagnosticCreator<T extends { [code: string]: DiagnosticMessages }>(
diagnostics: DiagnosticMap<T>,
libraryName?: string,
referenceDocsBaseUrl?: string,
): DiagnosticCreator<T> {
const errorMessage = libraryName
? `It must match one of the code defined in the library '${libraryName}'`
Expand Down Expand Up @@ -59,6 +60,9 @@ export function createDiagnosticCreator<T extends { [code: string]: DiagnosticMe
};
if (diagnosticDef.url) {
mutate(result).url = diagnosticDef.url;
} else if (diagnosticDef.docs && referenceDocsBaseUrl) {
mutate(result).url =
`${referenceDocsBaseUrl.replace(/\/$/, "")}/diagnostics/${String(diagnostic.code)}`;
}
if (diagnostic.codefixes) {
mutate(result).codefixes = diagnostic.codefixes;
Expand Down
31 changes: 31 additions & 0 deletions packages/compiler/src/core/file-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* A lazy reference to a file on disk. Unlike embedding the file content directly, a
* `FileRef` only describes *where* the content lives; it is resolved (read) by tooling
* such as `tspd` when needed. Because it does not read the file at definition time, it is
* safe to include in code that is bundled for the browser (e.g. the playground).
*/
export interface FileRef {
readonly kind: "file-ref";
/** Path to the file, relative to the package root. */
readonly path: string;
}

export const fileRef = {
/**
* Create a {@link FileRef} pointing to a file relative to the package root (the directory
* containing the library's `package.json`).
*
* @example
* ```ts
* docs: fileRef.fromPackageRoot("src/rules/my-rule.md"),
* ```
*/
fromPackageRoot(path: string): FileRef {
return { kind: "file-ref", path };
},
};

/** Type guard for {@link FileRef}. */
export function isFileRef(value: unknown): value is FileRef {
return typeof value === "object" && value !== null && (value as FileRef).kind === "file-ref";
}
6 changes: 5 additions & 1 deletion packages/compiler/src/core/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export function createTypeSpecLibrary<
>(lib: Readonly<TypeSpecLibraryDef<T, E, State>>): TypeSpecLibrary<T, E, State> {
let emitterOptionValidator: JSONSchemaValidator;

const { reportDiagnostic, createDiagnostic } = createDiagnosticCreator(lib.diagnostics, lib.name);
const { reportDiagnostic, createDiagnostic } = createDiagnosticCreator(
lib.diagnostics,
lib.name,
lib.referenceDocs?.baseUrl,
);

function createStateSymbol(name: string): symbol {
return Symbol.for(`${lib.name}.${name}`);
Expand Down
7 changes: 6 additions & 1 deletion packages/compiler/src/core/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ export interface LinterResult {
export function resolveLinterDefinition(
libName: string,
linter: LinterDefinition,
referenceDocsBaseUrl?: string,
): LinterResolvedDefinition {
const rules: LinterRule<string, any>[] = linter.rules.map((rule) => {
return { ...rule, id: `${libName}/${rule.name}` };
const resolved: LinterRule<string, any> = { ...rule, id: `${libName}/${rule.name}` };
if (resolved.url === undefined && resolved.docs && referenceDocsBaseUrl) {
resolved.url = `${referenceDocsBaseUrl.replace(/\/$/, "")}/rules/${rule.name}`;
}
return resolved;
});
if (linter.rules.length === 0 || (linter.ruleSets && "all" in linter.ruleSets)) {
return {
Expand Down
50 changes: 49 additions & 1 deletion packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
JsSourceFileNode,
LibraryInstance,
LibraryMetadata,
LinterDefinition,
LiteralType,
LocationContext,
LogSink,
Expand Down Expand Up @@ -134,12 +135,33 @@ export interface Program {
/** Return location context of the given source file. */
getSourceFileLocationContext(sourceFile: SourceFile): LocationContext;

/**
* Get information about a library loaded during this compilation, by its package name.
* Used by tooling (e.g. the language server) to resolve reference documentation.
* @internal
*/
getLoadedLibraryInfo(name: string): Promise<LoadedLibraryInfo | undefined>;

/**
* Project root. If a tsconfig was found/specified this is the directory for the tsconfig.json. Otherwise directory where the entrypoint is located.
*/
readonly projectRoot: string;
}

/**
* Information about a library loaded during compilation, used by tooling (e.g. the
* language server) to resolve reference documentation without re-resolving or reloading it.
* @internal
*/
export interface LoadedLibraryInfo {
/** Absolute path to the package root (the folder containing the library's package.json). */
readonly packageRoot: string;
/** The library's `$lib` export, if any. */
readonly $lib?: TypeSpecLibrary<any>;
/** The library's `$linter` export, if any. */
readonly $linter?: LinterDefinition;
}

interface EmitterRef {
emitFunction: EmitterFunc;
main: string;
Expand Down Expand Up @@ -271,6 +293,7 @@ async function createProgram(
/** @internal */
resolveTypeOrValueReference,
getSourceFileLocationContext,
getLoadedLibraryInfo,
projectRoot: getDirectoryPath(options.config ?? resolvedMain ?? ""),
};

Expand Down Expand Up @@ -492,6 +515,25 @@ async function createProgram(
return locationContext;
}

async function getLoadedLibraryInfo(name: string): Promise<LoadedLibraryInfo | undefined> {
const ref = sourceResolution.loadedLibraries.get(name);
if (ref === undefined) {
return undefined;
}
// Load the library's default entrypoint (which exports `$lib` and `$linter`) the same way the
// compiler does. `loadJsFile` is cached, so for a library already loaded during this
// compilation this does not re-read or re-evaluate anything. Note the entry imported by
// `.tsp` files (the `typespec` condition) may only expose `$lib`, so we can't rely on the
// already-parsed `jsSourceFiles` to also carry `$linter`.
const [resolution] = await resolveEmitterModuleAndEntrypoint(basedir, name);
const esmExports = resolution?.entrypoint?.esmExports;
return {
packageRoot: ref.path,
$lib: esmExports?.$lib,
$linter: esmExports?.$linter,
};
}

async function loadEmitters(
basedir: string,
emitterNameOrPaths: string[],
Expand Down Expand Up @@ -548,7 +590,13 @@ async function createProgram(
...resolution,
metadata,
definition: libDefinition,
linter: linterDef && resolveLinterDefinition(libraryNameOrPath, linterDef),
linter:
linterDef &&
resolveLinterDefinition(
libraryNameOrPath,
linterDef,
libDefinition?.referenceDocs?.baseUrl,
),
};
}

Expand Down
25 changes: 25 additions & 0 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JSONSchemaType as AjvJSONSchemaType } from "ajv";
import type { ModuleResolutionResult } from "../module-resolver/index.js";
import type { YamlPathTarget, YamlScript } from "../yaml/types.js";
import type { FileRef } from "./file-ref.js";
import type { Numeric } from "./numeric.js";
import type { Program } from "./program.js";
import type { TokenFlags } from "./scanner.js";
Expand Down Expand Up @@ -2417,6 +2418,12 @@ export interface DiagnosticDefinition<M extends DiagnosticMessages> {
readonly description?: string;
/** Specifies the URL at which the full documentation can be accessed. */
readonly url?: string;
/**
* Extended documentation for this diagnostic. Surfaced both in generated reference
* documentation and in editor tooling (e.g. completion and hover). Either raw markdown,
* or a {@link FileRef} pointing to a markdown file (recommended).
*/
readonly docs?: string | FileRef;
}

export interface DiagnosticMessages {
Expand Down Expand Up @@ -2491,6 +2498,18 @@ export interface TypeSpecLibraryDef<
/** Optional registration of capabilities the library/emitter provides */
readonly capabilities?: TypeSpecLibraryCapabilities;

/**
* Reference documentation configuration for this library.
* When set, the compiler auto-generates the `url` of each documented diagnostic and linter
* rule that does not specify one explicitly, pointing at the generated reference pages:
* - diagnostics -> `${baseUrl}/diagnostics/<code>`
* - linter rules -> `${baseUrl}/rules/<name>`
*/
readonly referenceDocs?: {
/** Base URL of the library's generated reference documentation (e.g. its `.../reference` page). */
readonly baseUrl: string;
};

/**
* Map of potential diagnostics that can be emitted in this library where the key is the diagnostic code.
*/
Expand Down Expand Up @@ -2564,6 +2583,12 @@ interface LinterRuleDefinitionBase<
description: string;
/** Specifies the URL at which the full documentation can be accessed. */
url?: string;
/**
* Extended documentation for this rule. Surfaced both in generated reference
* documentation and in editor tooling (e.g. completion and hover). Either raw markdown,
* or a {@link FileRef} pointing to a markdown file (recommended).
*/
docs?: string | FileRef;
/** Messages that can be reported with the diagnostic. */
messages: DM;
/**
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export {
type WriteLine,
} from "./core/diagnostics.js";
export { emitFile, type EmitFileOptions, type NewLine } from "./core/emitter-utils.js";
export { fileRef, isFileRef, type FileRef } from "./core/file-ref.js";
export { checkFormatTypeSpec, formatTypeSpec } from "./core/formatter.js";
export {
DiscriminatedUnion,
Expand Down
Loading