Skip to content

Commit

Permalink
C-sharp Service emitter: Fix nullable types, anonymous types, and saf…
Browse files Browse the repository at this point in the history
…eInt (#5279)
  • Loading branch information
markcowl authored Dec 6, 2024
1 parent 47230dc commit c21b5c1
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 52 deletions.
8 changes: 8 additions & 0 deletions .chronus/changes/nullable-2024-11-6-1-23-27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/http-server-csharp"
---

Fix nullable types, anonymous types, and safeInt
36 changes: 34 additions & 2 deletions packages/http-server-csharp/src/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,13 +419,45 @@ export function getNumericConstraintAttribute(

export function getSafeIntAttribute(type: Scalar): Attribute | undefined {
if (type.name.toLowerCase() !== "safeint") return undefined;
return new Attribute(
const attr: Attribute = new Attribute(
new AttributeType({
name: "SafeInt",
name: `NumericConstraint<long>`,
namespace: HelperNamespace,
}),
[],
);

attr.parameters.push(
new Parameter({
name: "MinValue",
value: new NumericValue(-9007199254740991),
optional: true,
type: new CSharpType({
name: "long",
namespace: "System",
isBuiltIn: true,
isValueType: true,
isNullable: false,
}),
}),
);

attr.parameters.push(
new Parameter({
name: "MaxValue",
value: new NumericValue(9007199254740991),
optional: true,
type: new CSharpType({
name: "long",
namespace: "System",
isBuiltIn: true,
isValueType: true,
isNullable: false,
}),
}),
);

return attr;
}

function getEnumAttribute(type: Enum, cSharpName?: string): Attribute {
Expand Down
3 changes: 3 additions & 0 deletions packages/http-server-csharp/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ export class CSharpType implements CSharpTypeMetadata {
namespace: string;
isBuiltIn: boolean;
isValueType: boolean;
isNullable: boolean;

public constructor(input: {
name: string;
namespace: string;
isBuiltIn?: boolean;
isValueType?: boolean;
isNullable?: boolean;
}) {
this.name = input.name;
this.namespace = input.namespace;
this.isBuiltIn = input.isBuiltIn !== undefined ? input.isBuiltIn : input.namespace === "System";
this.isValueType = input.isValueType !== undefined ? input.isValueType : false;
this.isNullable = input.isNullable !== undefined ? input.isNullable : false;
}

isNamespaceInScope(scope?: Scope<string>, visited?: Set<Scope<string>>): boolean {
Expand Down
126 changes: 95 additions & 31 deletions packages/http-server-csharp/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
property,
property.name,
);
const [typeName, typeDefault] = this.#findPropertyType(property);

const [typeName, typeDefault, nullable] = this.#findPropertyType(property);
const doc = getDoc(this.emitter.getProgram(), property);
const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName);
// eslint-disable-next-line @typescript-eslint/no-deprecated
Expand All @@ -356,7 +357,9 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
: typeDefault;
return this.emitter.result
.rawCode(code`${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public ${this.#isInheritedProperty(property) ? "new " : ""}${typeName}${
property.optional && isValueType(this.emitter.getProgram(), property.type) ? "?" : ""
isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable)
? "?"
: ""
} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${
defaultValue ? ` = ${defaultValue};\n` : "\n"
}
Expand All @@ -365,14 +368,27 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)

#findPropertyType(
property: ModelProperty,
): [EmitterOutput<string>, string | boolean | undefined] {
): [EmitterOutput<string>, string | boolean | undefined, boolean] {
return this.#getTypeInfoForTsType(property.type);
}

#getTypeInfoForUnion(
union: Union,
): [EmitterOutput<string>, string | boolean | undefined, boolean] {
const propResult = this.#getNonNullableTsType(union);
if (propResult === undefined) {
return [
code`${emitter.emitTypeReference(union)}`,
undefined,
[...union.variants.values()].filter((v) => isNullType(v.type)).length > 0,
];
}
const [typeName, typeDefault, _] = this.#getTypeInfoForTsType(propResult.type);
return [typeName, typeDefault, propResult.nullable];
}
#getTypeInfoForTsType(
this: any,
tsType: Type,
): [EmitterOutput<string>, string | boolean | undefined] {
): [EmitterOutput<string>, string | boolean | undefined, boolean] {
function extractStringValue(type: Type, span: StringTemplateSpan): string {
switch (type.kind) {
case "String":
Expand Down Expand Up @@ -403,54 +419,62 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
}
switch (tsType.kind) {
case "String":
return [code`string`, `"${tsType.value}"`];
return [code`string`, `"${tsType.value}"`, false];
case "StringTemplate":
const template = tsType;
if (template.stringValue !== undefined)
return [code`string`, `"${template.stringValue}"`];
return [code`string`, `"${template.stringValue}"`, false];
const spanResults: string[] = [];
for (const span of template.spans) {
spanResults.push(extractStringValue(span, span));
}
return [code`string`, `"${spanResults.join("")}"`];
return [code`string`, `"${spanResults.join("")}"`, false];
case "Boolean":
return [code`bool`, `${tsType.value === true ? true : false}`];
return [code`bool`, `${tsType.value === true ? true : false}`, false];
case "Number":
const [type, value] = this.#findNumericType(tsType);
return [code`${type}`, `${value}`];
return [code`${type}`, `${value}`, false];
case "Tuple":
const defaults = [];
const [csharpType, isObject] = this.#coalesceTypes(tsType.values);
if (isObject) return ["object[]", undefined];
if (isObject) return ["object[]", undefined, false];
for (const value of tsType.values) {
const [_, itemDefault] = this.#getTypeInfoForTsType(value);
defaults.push(itemDefault);
}
return [code`${csharpType.getTypeReference()}[]`, `[${defaults.join(", ")}]`];
return [
code`${csharpType.getTypeReference()}[]`,
`[${defaults.join(", ")}]`,
csharpType.isNullable,
];
case "Object":
return [code`object`, undefined];
return [code`object`, undefined, false];
case "Model":
if (this.#isRecord(tsType)) {
return [code`JsonObject`, undefined];
return [code`JsonObject`, undefined, false];
}
return [code`${emitter.emitTypeReference(tsType)}`, undefined];
return [code`${emitter.emitTypeReference(tsType)}`, undefined, false];
case "ModelProperty":
return this.#getTypeInfoForTsType(tsType.type);
case "Enum":
return [code`${emitter.emitTypeReference(tsType)}`, undefined];
return [code`${emitter.emitTypeReference(tsType)}`, undefined, false];
case "EnumMember":
if (typeof tsType.value === "number") {
const stringValue = tsType.value.toString();
if (stringValue.includes(".") || stringValue.includes("e"))
return ["double", stringValue];
return ["int", stringValue];
return ["double", stringValue, false];
return ["int", stringValue, false];
}
if (typeof tsType.value === "string") {
return ["string", tsType.value];
return ["string", tsType.value, false];
}
return [code`object`, undefined];
return [code`object`, undefined, false];
case "Union":
return [code`${emitter.emitTypeReference(tsType)}`, undefined];
return this.#getTypeInfoForUnion(tsType);
case "UnionVariant":
return this.#getTypeInfoForTsType(tsType.type);
default:
return [code`${emitter.emitTypeReference(tsType)}`, undefined];
return [code`${emitter.emitTypeReference(tsType)}`, undefined, false];
}
}

Expand Down Expand Up @@ -753,13 +777,13 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
}
let i = 1;
for (const requiredParam of requiredParams) {
const [paramType, _] = this.#findPropertyType(requiredParam);
const [paramType, _, __] = this.#findPropertyType(requiredParam);
signature.push(
code`${paramType} ${ensureCSharpIdentifier(this.emitter.getProgram(), requiredParam, requiredParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`,
);
}
for (const optionalParam of optionalParams) {
const [paramType, _] = this.#findPropertyType(optionalParam);
const [paramType, _, __] = this.#findPropertyType(optionalParam);
signature.push(
code`${paramType}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`,
);
Expand Down Expand Up @@ -896,7 +920,7 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
name,
NameCasingType.Parameter,
);
let [emittedType, emittedDefault] = this.#findPropertyType(parameter);
let [emittedType, emittedDefault, _] = this.#findPropertyType(parameter);
if (emittedType.toString().endsWith("[]")) emittedDefault = undefined;
// eslint-disable-next-line @typescript-eslint/no-deprecated
const defaultValue = parameter.default
Expand All @@ -907,11 +931,18 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
code`${httpParam.type !== "path" ? this.#emitParameterAttribute(httpParam) : ""}${emittedType} ${emittedName}${defaultValue === undefined ? "" : ` = ${defaultValue}`}`,
);
}
#getBodyParameters(operation: HttpOperation): ModelProperty[] | undefined {
const bodyParam = operation.parameters.body;
if (bodyParam === undefined) return undefined;
if (bodyParam.property !== undefined) return [bodyParam.property];
if (bodyParam.type.kind !== "Model" || bodyParam.type.properties.size < 1) return undefined;
return [...bodyParam.type.properties.values()];
}

#emitOperationCallParameters(operation: HttpOperation): EmitterOutput<string> {
const signature = new StringBuilder();
const bodyParam = operation.parameters.body;
let i = 0;
const bodyParameters = this.#getBodyParameters(operation);
//const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path");
for (const parameter of operation.parameters.parameters) {
i++;
Expand All @@ -922,13 +953,27 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
) {
signature.push(
code`${this.#emitOperationCallParameter(operation, parameter)}${
i < operation.parameters.parameters.length || bodyParam !== undefined ? ", " : ""
i < operation.parameters.parameters.length || bodyParameters !== undefined ? ", " : ""
}`,
);
}
}
if (bodyParam !== undefined) {
signature.push(code`body`);
if (bodyParameters !== undefined) {
if (bodyParameters.length === 1) {
signature.push(code`body`);
} else {
let j = 0;
for (const parameter of bodyParameters) {
j++;
const propertyName = ensureCSharpIdentifier(
this.emitter.getProgram(),
parameter,
parameter.name,
NameCasingType.Property,
);
signature.push(code`body?.${propertyName}${j < bodyParameters.length ? ", " : ""}`);
}
}
}

return signature.reduce();
Expand Down Expand Up @@ -1148,6 +1193,14 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
return result;
}

#getNonNullableTsType(union: Union): { type: Type; nullable: boolean } | undefined {
const types = [...union.variants.values()];
const nulls = types.flatMap((v) => v.type).filter((t) => isNullType(t));
const nonNulls = types.flatMap((v) => v.type).filter((t) => !isNullType(t));
if (nonNulls.length === 1) return { type: nonNulls[0], nullable: nulls.length > 0 };
return undefined;
}

#coalesceTypes(types: Type[]): [CSharpType, boolean] {
const defaultValue: [CSharpType, boolean] = [
new CSharpType({
Expand All @@ -1158,8 +1211,9 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
true,
];
let current: CSharpType | undefined = undefined;
let nullable: boolean = false;
for (const type of types) {
let candidate: CSharpType;
let candidate: CSharpType | undefined = undefined;
switch (type.kind) {
case "Boolean":
candidate = new CSharpType({ name: "bool", namespace: "System", isValueType: true });
Expand All @@ -1186,14 +1240,24 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
case "Scalar":
candidate = getCSharpTypeForScalar(this.emitter.getProgram(), type);
break;
case "Intrinsic":
if (isNullType(type)) {
nullable = true;
candidate = current;
} else {
return defaultValue;
}
break;
default:
return defaultValue;
}

current = current ?? candidate;
if (current === undefined || !candidate.equals(current)) return defaultValue;
if (current === undefined || (candidate !== undefined && !candidate.equals(current)))
return defaultValue;
}

if (current !== undefined && nullable) current.isNullable = true;
return current === undefined ? defaultValue : [current, false];
}

Expand Down
Loading

0 comments on commit c21b5c1

Please sign in to comment.