Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 20, 2026

The emitter was directly reporting diagnostics to the program, preventing downstream emitters from collecting and handling diagnostics according to TypeSpec best practices ([Result, readonly Diagnostic[]] pattern).

Changes

  • createModel() - Now returns [CodeModel, readonly Diagnostic[]] using createDiagnosticCollector() and diagnostics.wrap()
  • createCodeModel() - New exported function wrapping emission logic, returns [void, readonly Diagnostic[]]
  • updateCodeModel parameter - Refactored from options to explicit parameter of createCodeModel() for better API discoverability
  • Logger class - Removed reportDiagnostic(), warn(), and error() methods; Logger now only handles tracing
  • Diagnostic collection pattern - Each function that returns diagnostics creates its own createDiagnosticCollector(). Internal helper functions accept DiagnosticCollector as a parameter. Callers use diagnostics.pipe() to unwrap tuple-returning functions.
  • _validateDotNetSdk() - Now returns [boolean, readonly Diagnostic[]] with its own diagnostic collector
  • $onEmit() - Refactored to call createCodeModel() and report collected diagnostics

All code that previously called logger.reportDiagnostic() now uses createDiagnostic() and adds diagnostics via DiagnosticCollector parameters. This implements the [Result, readonly Diagnostic[]] pattern consistently throughout the codebase, following the "contagious" pattern where functions that call diagnostic-returning functions must also return diagnostics.

The implementation follows TypeSpec best practices exactly as demonstrated in TCGC:

  • Functions that return [Result, readonly Diagnostic[]] create their own collector at the beginning
  • Helper functions accept DiagnosticCollector as a parameter and add diagnostics to it
  • Callers use diagnostics.pipe(func()) to unwrap results and aggregate diagnostics
  • Functions return diagnostics.wrap(result) at the end

Over 40 functions were updated across 8 files to properly handle diagnostic collection: type-converter.ts, service-authentication.ts, client-converter.ts, operation-converter.ts, namespace-converter.ts, emitter.ts, code-model-writer.ts, and example-converter.ts.

Usage

import { createCodeModel } from "@typespec/http-client-csharp";

export async function $onEmit(context: EmitContext<MyOptions>) {
  const updateCodeModel = (model: CodeModel, context: CSharpEmitterContext) => {
    // Customize the code model here
    return model;
  };
  const [, diagnostics] = await createCodeModel(context, updateCodeModel);
  // Handle diagnostics as needed
  context.program.reportDiagnostics(diagnostics);
}

Each function creates its own diagnostic collector and returns tuples following the TypeSpec pattern, enabling proper diagnostic composition for downstream emitters.

Original prompt

This section details on the original issue you should resolve

<issue_title>[http-client-csharp] export an API for downstream emitter to call</issue_title>
<issue_description>Originally posted: Azure/azure-sdk-for-net#54788

Per best practice of writing an emitter or typespec library, we should have signatures like this:

getStuff(): [Stuff, readonly Diagnostic[]]

Therefore here, we are making the following changes:

  1. in emitter.ts file, wrap everything inside the if (!program.compilerOptions.noEmit && !program.hasError()) into a new function, which returns [void, readonly Diagnostic[]].
  2. refactor the createCodeModel method to make it return [CodeModel, readonly Diagnostic[]] instead. Inside the createCodeModel method, see if there are code snippets calling the reportDiagnostic method, we need to change them to append the diagnostic into the array and return it.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits January 20, 2026 07:05
Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Copilot AI changed the title [WIP] Export an API for downstream emitter to call Export diagnostic-collecting API for downstream emitter composition Jan 20, 2026
Copilot AI requested a review from ArcturusZhang January 20, 2026 07:13

if (root) {
root = options["update-code-model"](root, sdkContext);
const updatedRoot = options["update-code-model"](root, sdkContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot could we remove this callback from the options, and put that as a parameter of this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! The update-code-model callback is now a parameter of createCodeModel() instead of being in options. This makes the API more explicit and allows downstream emitters to pass their own callback without modifying options. The option still exists for backward compatibility with $onEmit(). (commit feb41b6)

…odel parameter

Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Comment on lines 75 to 85
reportDiagnostic<C extends keyof DiagnosticMessagesMap, M extends keyof DiagnosticMessagesMap[C]>(
diag: DiagnosticReport<DiagnosticMessagesMap, C, M>,
): void {
libReportDiagnostic(this.program, diag);
if (this.collectedDiagnostics) {
// In collecting mode, store the diagnostic instead of reporting it
this.collectedDiagnostics.push(createDiagnostic(diag));
} else {
// In normal mode, report the diagnostic directly
libReportDiagnostic(this.program, diag);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should remove this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Removed the reportDiagnostic method (along with warn and error) from Logger class. Functions now create diagnostics using createDiagnostic() and add them directly to sdkContext.__diagnostics array, which is returned by createModel(). This follows the [Result, readonly Diagnostic[]] pattern throughout the codebase. (commit 750acf0)

Copy link
Member

@ArcturusZhang ArcturusZhang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we need to remove the method reportDiagnostic in Logger.
for those places that were previously calling this function, we need to change that method to use the same pattern to return [Result, readonly Diagnostic[]] instead.

Copy link
Contributor Author

Copilot AI commented Jan 27, 2026

@copilot we need to remove the method reportDiagnostic in Logger.
for those places that were previously calling this function, we need to change that method to use the same pattern to return `[Resu...

I understand you want to remove reportDiagnostic from Logger and have functions return [Result, readonly Diagnostic[]] instead. This requires refactoring multiple functions (fromSdkType, processServiceAuthentication, fromSdkClients, etc.) to collect and return diagnostics. This is a significant change affecting ~13 call sites across multiple files. I'll proceed with this refactoring systematically.

…tern

- Removed reportDiagnostic, warn, and error methods from Logger class
- Added __diagnostics array to CSharpEmitterContext for collecting diagnostics
- Updated all code to use createDiagnostic and push to sdkContext.__diagnostics
- Updated createModel to return diagnostics from __diagnostics instead of logger
- Updated all tests to report returned diagnostics to program for assertions
- Simplified Logger to only handle tracing, removed diagnostic collection mode

Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
@ArcturusZhang
Copy link
Member

@copilot I saw you added a diagnostic field in the context, but this is not quite following the best practice.
Please check the code here as examples: https://github.com/Azure/typespec-azure/blob/e80f70574c555eba434bad9ac67bdfee114a01ec/packages/typespec-client-generator-core/src/types.ts#L511
We should call

const diagnostics = createDiagnosticCollector();

at the beginning, and use its apis to wrap normal results without diagnostics, or add diagnostics into the result.

- Replaced __diagnostics array with DiagnosticCollector from TypeSpec
- Updated createModel to use diagnostics.wrap() for returning tuple
- Functions now access diagnostics via sdkContext.__diagnostics.add()
- Updated tests to set up diagnostic collector when needed
- Follows the pattern from TCGC: createDiagnosticCollector at top level

Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 27, 2026

@copilot I saw you added a diagnostic field in the context, but this is not quite following the best practice.
Please check the code here as examples: https://github.com/Azure/typespec-azure/blob/e80...

Refactored to use createDiagnosticCollector() following the TCGC pattern. The collector is created at the beginning of createModel(), attached to sdkContext.__diagnostics, and functions use diagnostics.add() to add diagnostics. The final result is returned using diagnostics.wrap(clientModel). (commit 4ba9cd2)

Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
@ArcturusZhang
Copy link
Member

@copilot I think you misunderstood the pattern.
The pattern is that, for every method that would return [Something, readonly Diagnostic[]], you should call the createDiagnosticCollector to handle those diagnostics.

If a method would be called here and it returns a diagnostic and something else, and we need this "something", we should do it in this way:

const result = diagnostics.pipe(theMethod());
// here theMethod returns [Something, readonly Diagnostic[]], and type of `result` is just `Something`

This pattern is kind of contagious, if one method is returning the diagnostics, its caller must also return the diagnostics in its signature.

Copilot AI and others added 2 commits January 27, 2026 09:41
…lection pattern

Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Co-authored-by: ArcturusZhang <10554446+ArcturusZhang@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 27, 2026

@copilot I think you misunderstood the pattern.
The pattern is that, for every method that would return [Something, readonly Diagnostic[]], you should call the createDiagnosticCollector to handle...

You're correct - I've now refactored to follow the proper pattern. Each function that returns diagnostics creates its own createDiagnosticCollector(), and callers use diagnostics.pipe() to unwrap tuple-returning functions. Internal helper functions accept DiagnosticCollector as a parameter. Removed __diagnostics from context - it's now only passed as parameters. All 172 tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[http-client-csharp] export an API for downstream emitter to call

2 participants