Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 15 additions & 2 deletions generators/csharp/base/src/context/GeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,11 +522,24 @@ export abstract class GeneratorContext extends AbstractGeneratorContext {
});
}

public getCurrentVersionValueAccess(): ast.CodeBlock {
/**
* @param inInterpolatedString wraps the access in parentheses, required inside an
* interpolated string hole where an unparenthesized `::` would otherwise start a
* format specifier
*/
public getCurrentVersionValueAccess({ inInterpolatedString = false } = {}): ast.CodeBlock {
return this.csharp.codeblock((writer) => {
writer.writeNode(this.Types.Version);
if (inInterpolatedString) {
writer.write("(");
}
// qualify globally so the reference cannot be shadowed by a constructor
// parameter or local named `Version`
writer.writeNode(this.Types.Version.asGloballyQualified());
writer.write(".");
writer.write(this.model.getPropertyNameFor(this.Types.Version.explicit("Current")));
if (inInterpolatedString) {
writer.write(")");
}
});
}

Expand Down
12 changes: 12 additions & 0 deletions generators/csharp/codegen/src/ast/types/ClassReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,18 @@ export class ClassReference extends Node implements Type {
});
}

/**
* returns this class reference as a fully qualified class reference with the `global::`
* alias, making it immune to shadowing by locals, parameters or members in scope
*/
public asGloballyQualified() {
return this.csharp.classReferenceInternal({
...this,
fullyQualified: true,
global: true
});
}

/** returns a class instantiation node for this class reference */
public new(args?: Omit<ClassInstantiation.Args, "classReference">) {
args = args ?? { arguments_: [] };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when
neither the constructor parameter nor the environment variable is set. The literal is
known at compile time, so it is now used as the header's default value.
type: fix

- summary: |
The generated `X-Fern-SDK-Version` header value is now emitted as a globally qualified
reference, so it can no longer be shadowed by a constructor parameter or local that
PascalCases to `Version` (previously `error CS1061: 'string' does not contain a
definition for 'Current'`).
type: fix
62 changes: 45 additions & 17 deletions generators/csharp/sdk/src/root-client/RootClientGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ interface HeaderInfo {
prefix?: string;
}

/** The compile-time value of a `literal<"...">`-typed global header, if this parameter is one. */
function getLiteralHeaderValue(param: ConstructorParameter): Literal | undefined {
if (!param.isGlobalHeader) {
return undefined;
}
const { typeReference } = param;
return typeReference.type === "container" && typeReference.container.type === "literal"
? typeReference.container.literal
: undefined;
}

export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorContext> {
private rawClient: RawClient;
private serviceId: ServiceId | undefined;
Expand Down Expand Up @@ -474,7 +485,7 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
userAgent: platformHeaders.userAgent,
packageName: this.generation.names.project.packageId,
csharp: this.csharp,
versionValueAccess: this.context.getCurrentVersionValueAccess(),
versionValueAccess: this.context.getCurrentVersionValueAccess({ inInterpolatedString: true }),
userAgentNameFromPackage: this.settings.userAgentNameFromPackage
});
if (userAgentEntry != null) {
Expand Down Expand Up @@ -539,10 +550,7 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
}

for (const param of optionalParameters) {
const clientDefaultLiteral =
param.isGlobalHeader && param.clientDefault != null
? this.getHeaderFallback(param)
: undefined;
const clientDefaultLiteral = this.getGlobalHeaderDefaultLiteral(param);
if (param.environmentVariable != null) {
const target = paramAccess(param);
if (anyAuthMultiScheme || endpointSecurity || (param.isGlobalHeader && param.isOptional)) {
Expand Down Expand Up @@ -1503,43 +1511,63 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC

private getParameterForHeader(header: HttpHeader): ConstructorParameter {
const hasClientDefault = header.clientDefault != null;
const literal =
header.valueType.type === "container" && header.valueType.container.type === "literal"
? header.valueType.container.literal
: undefined;
// env vars are strings, so only a string-typed literal can be resolved from one; other
// literals stay out of the constructor and are surfaced through ClientOptions instead.
const environmentVariable = literal == null || literal.type === "string" ? header.env : undefined;
return {
name:
header.valueType.type === "container" && header.valueType.container.type === "literal"
? this.case.pascalSafe(header.name)
: this.case.camelSafe(header.name),
name: literal != null ? this.case.pascalSafe(header.name) : this.case.camelSafe(header.name),
header: {
name: getWireValue(header.name)
},
docs: header.docs,
// a literal-typed header's value is known at compile time, so once an env var promotes it
// to a constructor parameter it is always optional: it falls back to the literal rather
// than requiring the caller or the environment to supply a value.
isOptional:
hasClientDefault ||
(header.valueType.type === "container" && header.valueType.container.type === "optional"),
(header.valueType.type === "container" && header.valueType.container.type === "optional") ||
(literal != null && environmentVariable != null),
typeReference: header.valueType,
type: this.context.csharpTypeMapper.convert({
reference: header.valueType
}),
environmentVariable: header.env,
environmentVariable,
isGlobalHeader: true,
exampleValue: this.case.screamingSnakeSafe(header.name),
clientDefault: header.clientDefault
};
}

private getHeaderFallback(param: ConstructorParameter): string {
if (param.clientDefault != null) {
switch (param.clientDefault.type) {
const default_ = param.clientDefault ?? getLiteralHeaderValue(param);
if (default_ != null) {
switch (default_.type) {
case "string":
return `"${escapeForCSharpString(param.clientDefault.string)}"`;
return `"${escapeForCSharpString(default_.string)}"`;
case "boolean":
return param.clientDefault.boolean ? `"${true.toString()}"` : `"${false.toString()}"`;
return default_.boolean ? `"${true.toString()}"` : `"${false.toString()}"`;
default:
assertNever(param.clientDefault);
assertNever(default_);
}
}
return `""`;
}

/**
* The compile-time default to fall back to for a global header parameter: its `client-default`,
* or the literal value when the header is literal-typed.
*/
private getGlobalHeaderDefaultLiteral(param: ConstructorParameter): string | undefined {
if (!param.isGlobalHeader || (param.clientDefault == null && getLiteralHeaderValue(param) == null)) {
return undefined;
}
return this.getHeaderFallback(param);
}

private getFromEnvironmentOrThrowMethod(cls: ast.Class) {
cls.addMethod({
access: ast.Access.Private,
Expand Down Expand Up @@ -1595,7 +1623,7 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
writer.write(buildUserAgentReturnPrefix(productName));
// Written via `writeNode` so the generated `Version` reference
// registers its using directive.
writer.writeNode(this.context.getCurrentVersionValueAccess());
writer.writeNode(this.context.getCurrentVersionValueAccess({ inInterpolatedString: true }));
writer.writeLine(BUILD_USER_AGENT_RETURN_SUFFIX);
}),
type: ast.MethodType.STATIC
Expand Down
15 changes: 15 additions & 0 deletions generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.82.1
changelogEntry:
- summary: |
Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when
neither the constructor parameter nor the environment variable is set. The literal is
known at compile time, so it is now used as the header's default value.
type: fix
- summary: |
The generated `X-Fern-SDK-Version` header value is now emitted as a globally qualified
reference, so it can no longer be shadowed by a constructor parameter or local that
PascalCases to `Version` (previously `error CS1061: 'string' does not contain a
definition for 'Current'`).
type: fix
createdAt: "2026-08-18"
irVersion: 67
- version: 2.82.0
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when
neither the constructor parameter nor the environment variable is set. The literal is
known at compile time, so it is now used as the header's default value.
type: fix
11 changes: 9 additions & 2 deletions generators/php/sdk/src/root-client/RootClientGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,10 @@ export class RootClientGenerator extends FileGenerator<PhpFile, SdkCustomConfigS
}

private getParameterForHeader(header: FernIr.HttpHeader): ConstructorParameter {
const literal = this.context.maybeLiteral(header.valueType);
// Env vars are strings, so only a string-typed literal can be resolved from one. Other
// literals stay out of the constructor's env-resolution path and remain literal parameters.
const environmentVariable = literal == null || literal.type === "string" ? header.env : undefined;
return {
name: this.context.getParameterName(header.name),
header: {
Expand All @@ -1472,9 +1476,12 @@ export class RootClientGenerator extends FileGenerator<PhpFile, SdkCustomConfigS
docs: header.docs,
isOptional: this.context.isOptional(header.valueType),
typeReference: header.valueType,
environmentVariable: header.env,
environmentVariable,
isGlobalHeader: true,
clientDefault: header.clientDefault
// A literal-typed header's value is known at compile time, so when an env var promotes
// it to a constructor parameter the literal acts as its client default: the parameter
// stays optional and falls back to the literal instead of throwing.
clientDefault: header.clientDefault ?? (environmentVariable != null ? literal : undefined)
};
}

Expand Down
9 changes: 9 additions & 0 deletions generators/php/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.20.2
changelogEntry:
- summary: |
Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when
neither the constructor parameter nor the environment variable is set. The literal is
known at compile time, so it is now used as the header's default value.
type: fix
createdAt: "2026-08-18"
irVersion: 67
- version: 2.20.1
changelogEntry:
- summary: |
Expand Down
Loading
Loading