Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d0c169b
Initial plan
Copilot Jan 20, 2026
b0bde0a
Refactor emitter to export API with diagnostic return values
Copilot Jan 20, 2026
7c36f32
Add tests for new diagnostic collection API
Copilot Jan 20, 2026
d368866
Add documentation for new diagnostic collection API
Copilot Jan 20, 2026
feb41b6
Refactor: move update-code-model callback from options to createCodeM…
Copilot Jan 27, 2026
750acf0
Remove reportDiagnostic from Logger and use diagnostic collection pat…
Copilot Jan 27, 2026
4ba9cd2
Use createDiagnosticCollector following TypeSpec best practices
Copilot Jan 27, 2026
97af9e8
Fix: use void 0 instead of undefined as void cast
Copilot Jan 27, 2026
bde168c
Refactor http-client-csharp emitter to follow TypeSpec diagnostic col…
Copilot Jan 27, 2026
3661728
Fix remaining tests for diagnostic collection pattern
Copilot Jan 27, 2026
433e28f
Fix diagnostic collection pattern to follow TypeSpec best practices
Copilot Jan 28, 2026
1bde6f8
Refactor createCodeModel to use diagnostic collector pattern properly
Copilot Jan 28, 2026
1422867
Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.g…
Copilot Jan 28, 2026
59709e0
Final cleanup: revert writeCodeModel and remove update-code-model option
Copilot Jan 28, 2026
239dba6
Add back updateCodeModel callback parameter to createCodeModel
Copilot Jan 28, 2026
4569ec6
Rename createCodeModel to $emitCodeModel and export it
Copilot Jan 29, 2026
d0c4c82
Add comprehensive documentation for downstream emitter usage
Copilot Jan 30, 2026
cad76cc
Remove documentation file and rename $emitCodeModel to emitCodeModel
Copilot Jan 30, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import { UsageFlags } from "@azure-tools/typespec-client-generator-core";
import { resolvePath } from "@typespec/compiler";
import { createDiagnosticCollector, Diagnostic, NoTarget, resolvePath } from "@typespec/compiler";
import { configurationFileName, tspOutputFileName } from "./constants.js";
import { createDiagnostic } from "./lib/lib.js";
import { CSharpEmitterContext } from "./sdk-context.js";
import { CodeModel } from "./type/code-model.js";
import { Configuration } from "./type/configuration.js";
Expand Down
132 changes: 93 additions & 39 deletions packages/http-client-csharp/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { createSdkContext, SdkContext } from "@azure-tools/typespec-client-generator-core";
import {
createDiagnosticCollector,
Diagnostic,
EmitContext,
getDirectoryPath,
joinPaths,
Expand All @@ -20,11 +22,13 @@ import {
tspOutputFileName,
} from "./constants.js";
import { createModel } from "./lib/client-model-builder.js";
import { createDiagnostic } from "./lib/lib.js";
import { LoggerLevel } from "./lib/logger-level.js";
import { Logger } from "./lib/logger.js";
import { execAsync, execCSharpGenerator } from "./lib/utils.js";
import { CSharpEmitterOptions, resolveOptions } from "./options.js";
import { createCSharpEmitterContext, CSharpEmitterContext } from "./sdk-context.js";
import { CodeModel } from "./type/code-model.js";
import { Configuration } from "./type/configuration.js";

/**
Expand All @@ -48,11 +52,34 @@ function findProjectRoot(path: string): string | undefined {
}

/**
* The entry point for the emitter. This function is called by the typespec compiler.
* Creates a code model by executing the full emission logic.
* This function can be called by downstream emitters to generate a code model and collect diagnostics.
*
* @example
* ```typescript
* import { emitCodeModel } from "@typespec/http-client-csharp";
*
* export async function $onEmit(context: EmitContext<MyEmitterOptions>) {
* const updateCodeModel = (model: CodeModel, context: CSharpEmitterContext) => {
* // Customize the code model here
* return model;
* };
* const [, diagnostics] = await emitCodeModel(context, updateCodeModel);
* // Process diagnostics as needed
* context.program.reportDiagnostics(diagnostics);
* }
* ```
*
* @param context - The emit context
* @param updateCodeModel - Optional callback to modify the code model before emission
* @returns A tuple containing void and any diagnostics that were generated during the emission
* @beta
*/
export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
export async function emitCodeModel(
context: EmitContext<CSharpEmitterOptions>,
updateCodeModel?: (model: CodeModel, context: CSharpEmitterContext) => CodeModel,
): Promise<[void, readonly Diagnostic[]]> {
const diagnostics = createDiagnosticCollector();
const program: Program = context.program;
const options = resolveOptions(context);
const outputFolder = context.emitterOutputDir;
Expand All @@ -70,22 +97,26 @@ export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
),
logger,
);
program.reportDiagnostics(sdkContext.diagnostics);
for (const diag of sdkContext.diagnostics) {
diagnostics.add(diag);
}

let root = createModel(sdkContext);
const root = diagnostics.pipe(createModel(sdkContext));

if (root) {
root = options["update-code-model"](root, sdkContext);
// Apply optional code model update callback
const updatedRoot = updateCodeModel ? updateCodeModel(root, sdkContext) : root;

const generatedFolder = resolvePath(outputFolder, "src", "Generated");

if (!fs.existsSync(generatedFolder)) {
fs.mkdirSync(generatedFolder, { recursive: true });
}

// emit tspCodeModel.json
await writeCodeModel(sdkContext, root, outputFolder);
await writeCodeModel(sdkContext, updatedRoot, outputFolder);

const namespace = root.name;
const namespace = updatedRoot.name;
const configurations: Configuration = createConfiguration(options, namespace, sdkContext);

//emit configuration.json
Expand Down Expand Up @@ -113,7 +144,7 @@ export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
debug: options.debug ?? false,
});
if (result.exitCode !== 0) {
const isValid = await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion);
const isValid = diagnostics.pipe(await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion));
// if the dotnet sdk is valid, the error is not dependency issue, log it as normal
if (isValid) {
throw new Error(
Expand All @@ -122,7 +153,7 @@ export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
}
}
} catch (error: any) {
const isValid = await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion);
const isValid = diagnostics.pipe(await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion));
// if the dotnet sdk is valid, the error is not dependency issue, log it as normal
if (isValid) throw new Error(error);
}
Expand All @@ -133,6 +164,18 @@ export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
}
}
}

return diagnostics.wrap(undefined);
}

/**
* The entry point for the emitter. This function is called by the typespec compiler.
* @param context - The emit context
* @beta
*/
export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
const [, diagnostics] = await emitCodeModel(context);
context.program.reportDiagnostics(diagnostics);
}

export function createConfiguration(
Expand All @@ -142,7 +185,6 @@ export function createConfiguration(
): Configuration {
const skipKeys = [
"new-project",
"update-code-model",
"sdk-context-options",
"save-inputs",
"generator-name",
Expand Down Expand Up @@ -172,62 +214,74 @@ export function createConfiguration(
* Report diagnostic if dotnet sdk is not installed or its version does not meet prerequisite
* @param sdkContext - The SDK context
* @param minVersionRequisite - The minimum required major version
* @param logger - The logger
* @returns A tuple containing whether the SDK is valid and any diagnostics
* @internal
*/
export async function _validateDotNetSdk(
sdkContext: CSharpEmitterContext,
minMajorVersion: number,
): Promise<boolean> {
): Promise<[boolean, readonly Diagnostic[]]> {
const diagnostics = createDiagnosticCollector();
try {
const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" });
return validateDotNetSdkVersionCore(sdkContext, result.stdout, minMajorVersion);
return diagnostics.wrap(diagnostics.pipe(validateDotNetSdkVersionCore(sdkContext, result.stdout, minMajorVersion)));
} catch (error: any) {
if (error && "code" in error && error["code"] === "ENOENT") {
sdkContext.logger.reportDiagnostic({
code: "invalid-dotnet-sdk-dependency",
messageId: "missing",
format: {
dotnetMajorVersion: `${minMajorVersion}`,
downloadUrl: "https://dotnet.microsoft.com/",
},
target: NoTarget,
});
diagnostics.add(
createDiagnostic({
code: "invalid-dotnet-sdk-dependency",
messageId: "missing",
format: {
dotnetMajorVersion: `${minMajorVersion}`,
downloadUrl: "https://dotnet.microsoft.com/",
},
target: NoTarget,
}),
);
}
return false;
return diagnostics.wrap(false);
}
}

function validateDotNetSdkVersionCore(
sdkContext: CSharpEmitterContext,
version: string,
minMajorVersion: number,
): boolean {
): [boolean, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
if (version) {
const dotIndex = version.indexOf(".");
const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex);
const major = Number(firstPart);

if (isNaN(major)) {
return false;
return diagnostics.wrap(false);
}
if (major < minMajorVersion) {
sdkContext.logger.reportDiagnostic({
code: "invalid-dotnet-sdk-dependency",
messageId: "invalidVersion",
format: {
installedVersion: version,
dotnetMajorVersion: `${minMajorVersion}`,
downloadUrl: "https://dotnet.microsoft.com/",
},
target: NoTarget,
});
return false;
diagnostics.add(
createDiagnostic({
code: "invalid-dotnet-sdk-dependency",
messageId: "invalidVersion",
format: {
installedVersion: version,
dotnetMajorVersion: `${minMajorVersion}`,
downloadUrl: "https://dotnet.microsoft.com/",
},
target: NoTarget,
}),
);
return diagnostics.wrap(false);
}
return true;
return diagnostics.wrap(true);
} else {
sdkContext.logger.error("Cannot get the installed .NET SDK version.");
return false;
diagnostics.add(
createDiagnostic({
code: "general-error",
format: { message: "Cannot get the installed .NET SDK version." },
target: NoTarget,
}),
);
return diagnostics.wrap(false);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/http-client-csharp/emitter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

export { writeCodeModel } from "./code-model-writer.js";
export { configurationFileName, tspOutputFileName } from "./constants.js";
export { $onEmit } from "./emitter.js";
export { emitCodeModel, $onEmit } from "./emitter.js";
// we export `createModel` only for autorest.csharp because it uses the emitter to generate the code model file but not calling the dll here
// we could remove this export when in the future we deprecate autorest.csharp
export { createModel } from "./lib/client-model-builder.js";
Expand Down
Loading
Loading