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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ describe("Discriminator property", () => {
runner = await createEmitterTestHost();
});

it.each([undefined, "", 'Use "Current" instead.\nSee C:\\sdk.'])(
"preserves model and enum deprecation metadata: %s",
async (message) => {
const directive = message === undefined ? "" : `#deprecated ${JSON.stringify(message)}`;
const program = await typeSpecCompile(
`${directive}
enum LegacyChoice { one, two }
${directive}
model Legacy { choice: LegacyChoice; }
#suppress "deprecated" "Testing deprecated model references."
@get op read(): Legacy;`,
runner,
);
const sdkContext = await createCSharpSdkContext(createEmitterContext(program));
const [root] = createModel(sdkContext);
strictEqual(root.models.find((m) => m.name === "Legacy")?.deprecation, message);
strictEqual(root.enums.find((e) => e.name === "LegacyChoice")?.deprecation, message);
},
);

it("Base model has discriminator property", async () => {
const program = await typeSpecCompile(
`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ describe("Operation Converter", () => {
runner = await createEmitterTestHost();
});

it.each([undefined, "", 'Use "read" instead.\nSee C:\\sdk.'])(
"preserves operation deprecation metadata: %s",
async (message) => {
const program = await typeSpecCompile(
`${message === undefined ? "" : `#deprecated ${JSON.stringify(message)}`}
@get op legacy(): string;`,
runner,
);
const sdkContext = await createCSharpSdkContext(createEmitterContext(program));
const [root] = createModel(sdkContext);
strictEqual(root.clients[0].methods[0].operation.deprecated, message);
const serialized = JSON.parse(serializeCodeModel(sdkContext, root));
strictEqual(serialized.clients[0].methods[0].operation.deprecated, message);
},
);

it.each([
{ start: 200, end: 299 },
{ start: 204, end: 204 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,23 +618,22 @@ private static void AddAttributeForType(
TypeProvider typeProvider,
string key)
{
AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes
.FirstOrDefault(a => ExperimentalApiHelpers.IsExperimentalAttribute(a) || a.Type.Equals(typeof(ObsoleteAttribute)));

if (experimentalOrObsoleteAttribute is not null && ExperimentalApiHelpers.IsExperimentalAttribute(experimentalOrObsoleteAttribute))
{
string justification = $"{typeProvider.Type} is experimental and may change in future versions.";
attributes.Add(key, new SuppressionStatement(attributeStatement, experimentalOrObsoleteAttribute.Arguments[0], justification));
}
else if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ObsoleteAttribute)) == true)
{
string justification = $"{typeProvider.Type} is obsolete and may be removed in future versions.";
attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(DefaultObsoleteDiagnosticId), justification));
}
else
var suppressions = new List<SuppressionStatement>();
foreach (var attribute in typeProvider.CanonicalView.Attributes)
{
attributes.Add(key, attributeStatement);
if (ExperimentalApiHelpers.IsExperimentalAttribute(attribute))
{
string justification = $"{typeProvider.Type} is experimental and may change in future versions.";
suppressions.Add(new SuppressionStatement(null, attribute.Arguments[0], justification));
}
else if (attribute.Type.Equals(typeof(ObsoleteAttribute)))
{
string justification = $"{typeProvider.Type} is obsolete and may be removed in future versions.";
suppressions.Add(new SuppressionStatement(null,
Literal(DeprecationApiHelpers.GetDiagnosticId(attribute) ?? DefaultObsoleteDiagnosticId), justification));
}
}
attributes.Add(key, ExperimentalApiHelpers.Suppress(attributeStatement, suppressions));
}

private static void AddAttributeForType(
Expand All @@ -645,6 +644,7 @@ private static void AddAttributeForType(
string obsoleteTypeJustification)
{
var key = frameworkType.FullName ?? frameworkType.Name;
var suppressions = new List<SuppressionStatement>();

// Match [Experimental] by attribute type full name rather than runtime identity. Dependencies that
// target netstandard2.0 polyfill their own System.Diagnostics.CodeAnalysis.ExperimentalAttribute,
Expand All @@ -658,8 +658,7 @@ private static void AddAttributeForType(
var diagnosticId = experimentalData.ConstructorArguments.Count > 0
? experimentalData.ConstructorArguments[0].Value as string
: null;
attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(diagnosticId), experimentalTypeJustification));
return;
suppressions.Add(new SuppressionStatement(null, Literal(diagnosticId), experimentalTypeJustification));
}

var obsoleteAttr = frameworkType.GetCustomAttributes(typeof(ObsoleteAttribute), false)
Expand All @@ -668,11 +667,10 @@ private static void AddAttributeForType(
{
var diagnosticId = obsoleteAttr.GetType().GetProperty("DiagnosticId")?.GetValue(obsoleteAttr)
?? DefaultObsoleteDiagnosticId;
attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(diagnosticId), obsoleteTypeJustification));
return;
suppressions.Add(new SuppressionStatement(null, Literal(diagnosticId), obsoleteTypeJustification));
}

attributes.Add(key, attributeStatement);
attributes.Add(key, ExperimentalApiHelpers.Suppress(attributeStatement, suppressions));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod,
}

ApplyExperimentalAttributes(methodSignature, customSignature, BuildConvenienceMethodAttributes() ?? []);
ApplyDeprecationAttribute(methodSignature, customSignature);

// Recompute the response body type so we can branch the body accordingly.
GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out var responseBodyType);
Expand Down Expand Up @@ -299,6 +300,16 @@ .. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync,

var convenienceMethod = new ScmMethodProvider(methodSignature, methodBody, EnclosingType, ScmMethodKind.Convenience, collectionDefinition: collection, serviceMethod: ServiceMethod);
ExperimentalApiHelpers.AddDependencySuppressions(convenienceMethod, ServiceMethod.Operation);
var customProtocol = client.CustomCodeView?.Methods.FirstOrDefault(method =>
MethodSignatureBase.SignatureComparer.Equals(method.Signature, protocolMethod.Signature));
var declarationSuppressions = ExperimentalApiHelpers.GetAttributeSuppressions(
methodSignature.Attributes.Concat(customSignature?.Attributes ?? []));
convenienceMethod.Update(suppressions: ExperimentalApiHelpers.MergeSuppressions(
convenienceMethod.Suppressions,
ExperimentalApiHelpers.GetAttributeSuppressions(
protocolMethod.Signature.Attributes.Concat(customProtocol?.Signature.Attributes ?? []))
.ExceptBy(declarationSuppressions.Select(suppression => suppression.Code.ToDisplayString()),
suppression => suppression.Code.ToDisplayString())));

if (convenienceMethod.XmlDocs != null)
{
Expand All @@ -314,6 +325,7 @@ private MethodSignatureModifiers GetConvenienceMethodModifiers(
MethodSignatureModifiers modifiers,
IReadOnlyList<ParameterProvider> signatureParameters)
{
modifiers &= ~MethodSignatureModifiers.Partial;
// The convenience method's public/internal accessibility follows the service method's
// accessibility, independent of the protocol method. The protocol method may be internal
// (e.g. when `@protocolAPI(false)` sets GenerateProtocolMethod to false) while the
Expand Down Expand Up @@ -1138,14 +1150,20 @@ private static void ApplyExperimentalAttributes(
MethodSignature? customSignature,
IReadOnlyList<AttributeStatement> generatedAttributes)
{
if (customSignature?.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute) == true)
if (customSignature?.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute) != true
&& generatedAttributes.Count > 0)
{
// The defining partial declaration already carries the custom experiment.
signature.Update(attributes: [.. signature.Attributes.Where(a => !ExperimentalApiHelpers.IsExperimentalAttribute(a))]);
signature.Update(attributes: [.. signature.Attributes, .. generatedAttributes]);
}
else if (generatedAttributes.Count > 0)
}

private void ApplyDeprecationAttribute(MethodSignature signature, MethodSignature? customSignature)
{
if (customSignature?.Attributes.Any(DeprecationApiHelpers.IsObsoleteAttribute) != true
&& !signature.Attributes.Any(DeprecationApiHelpers.IsObsoleteAttribute)
&& DeprecationApiHelpers.BuildAttribute(ServiceMethod.Operation.Deprecated) is { } attribute)
{
signature.Update(attributes: [.. signature.Attributes, .. generatedAttributes]);
signature.Update(attributes: [.. signature.Attributes, attribute]);
}
}

Expand Down Expand Up @@ -1492,6 +1510,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod
}

ApplyExperimentalAttributes(methodSignature, customSignature, ExperimentalApiHelpers.BuildAttributes(ServiceMethod.Operation.Experimental));
ApplyDeprecationAttribute(methodSignature, customSignature);

TypeProvider? collection = null;
MethodBodyStatement[] methodBody;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.ClientProvide
{
public class ClientProviderTests
{
[Test]
public void ClientProviderLookupFollowsIdentityUpdates()
{
var input = InputFactory.Client("OriginalClient");
MockHelpers.LoadMockGenerator(clients: () => [input]);
var factory = ScmCodeModelGenerator.Instance.TypeFactory;
var client = factory.CreateClient(input)!;
var originalIdentity = client.Type.WithNullable(true);

client.Update(name: "RenamedClient", @namespace: "Renamed");

Assert.IsFalse(factory.CSharpTypeMap.ContainsKey(originalIdentity));
Assert.AreSame(client, factory.CSharpTypeMap[client.Type.WithNullable(true)]);
}

[TestCase(false)]
[TestCase(true)]
public async Task ExperimentalPolyfilledCustomDeclarationsCompile(bool sourceAnnotated)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,43 @@ public void ExperimentalModelsHaveAttributeSuppression()
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
public async Task ExperimentalDeprecatedModelsHaveAllSuppressions()
{
var generated = InputFactory.Experimental(
InputFactory.Model("GeneratedCombinedModel", deprecation: "Use CurrentModel instead."),
"GENERATED001");
var customized = InputFactory.Model("CustomCombinedModel");
await MockHelpers.LoadMockGeneratorAsync(
inputModels: () => [generated, customized, InputFactory.Model("RegularModel")],
compilation: () => Helpers.GetCompilationFromDirectoryAsync());
var context = new ModelReaderWriterContextDefinition();

Assert.AreEqual(3, GetBuildableAttributes(context).Count);
Assert.AreEqual(Helpers.GetExpectedFromFile(), new TypeProviderWriter(context).Write().Content);
Assert.AreEqual(3, context.Attributes.Count);
}

[Test]
public void ExperimentalObsoleteFrameworkTypesHaveAllSuppressions()
{
var model = InputFactory.Model("ParentModel", properties:
[InputFactory.Property("Value", InputPrimitiveType.String)]);
MockHelpers.LoadMockGenerator(inputModels: () => [model],
createCSharpTypeCore: input =>
{
#pragma warning disable FRAMEWORK001, CS0618
return input == InputPrimitiveType.String
? new CSharpType(typeof(ExperimentalObsoleteFrameworkType))
: ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(input)!;
#pragma warning restore FRAMEWORK001, CS0618
});
var context = new ModelReaderWriterContextDefinition();

Assert.AreEqual(2, context.Attributes.Count);
Assert.AreEqual(Helpers.GetExpectedFromFile(), new TypeProviderWriter(context).Write().Content);
}

[Test]
public async Task NullableValueTypesAreHandledCorrectly()
{
Expand Down Expand Up @@ -1216,6 +1253,19 @@ public async Task ValidateCustomObsoleteTypeHasAttributeSuppression()
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
public async Task CustomObsoleteDiagnosticIdsAreSuppressed()
{
var model = InputFactory.Model("CustomizedObsoleteModel", deprecation: "Generated message");
await MockHelpers.LoadMockGeneratorAsync(
inputModels: () => [model, InputFactory.Model("RegularModel")],
compilation: () => Helpers.GetCompilationFromDirectoryAsync());
var context = new ModelReaderWriterContextDefinition();

Assert.AreEqual(2, context.Attributes.Count);
Assert.AreEqual(Helpers.GetExpectedFromFile(), new TypeProviderWriter(context).Write().Content);
}

[Test]
public void ValidateObsoleteGeneratedTypeHasAttributeSuppression()
{
Expand Down Expand Up @@ -1423,6 +1473,15 @@ protected internal override PropertyProvider[] BuildProperties()
}
}

[Experimental("FRAMEWORK001")]
[Obsolete("Use CurrentModel instead.")]
public class ExperimentalObsoleteFrameworkType : IPersistableModel<object>
{
public object Create(BinaryData data, ModelReaderWriterOptions options) => throw new NotImplementedException();
public string GetFormatFromOptions(ModelReaderWriterOptions options) => throw new NotImplementedException();
public BinaryData Write(ModelReaderWriterOptions options) => throw new NotImplementedException();
}

public class FrameworkModelWithMRW : IJsonModel<FrameworkModelWithMRW>, IPersistableModel<FrameworkModelWithMRW>
{
public string Value { get; set; } = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// <auto-generated/>

#nullable disable

using System.ClientModel.Primitives;
using Sample.Models;

namespace Sample
{
#pragma warning disable OBS001 // global::Sample.Models.CustomizedObsoleteModel is obsolete and may be removed in future versions.
[global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.CustomizedObsoleteModel))]
#pragma warning restore OBS001 // global::Sample.Models.CustomizedObsoleteModel is obsolete and may be removed in future versions.
[global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))]
public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Sample.Models
{
[Obsolete("Use Current instead.", DiagnosticId = "OBS001")]
public partial class CustomizedObsoleteModel
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// <auto-generated/>

#nullable disable

using System.ClientModel.Primitives;
using Sample.Models;

namespace Sample
{
#pragma warning disable CS0618 // global::Sample.Models.CustomCombinedModel is obsolete and may be removed in future versions.
#pragma warning disable CUSTOM001 // global::Sample.Models.CustomCombinedModel is experimental and may change in future versions.
[global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.CustomCombinedModel))]
#pragma warning restore CUSTOM001 // global::Sample.Models.CustomCombinedModel is experimental and may change in future versions.
#pragma warning restore CS0618 // global::Sample.Models.CustomCombinedModel is obsolete and may be removed in future versions.
#pragma warning disable GENERATED001 // global::Sample.Models.GeneratedCombinedModel is experimental and may change in future versions.
#pragma warning disable CS0618 // global::Sample.Models.GeneratedCombinedModel is obsolete and may be removed in future versions.
[global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.GeneratedCombinedModel))]
#pragma warning restore CS0618 // global::Sample.Models.GeneratedCombinedModel is obsolete and may be removed in future versions.
#pragma warning restore GENERATED001 // global::Sample.Models.GeneratedCombinedModel is experimental and may change in future versions.
[global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.RegularModel))]
public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics.CodeAnalysis;

namespace Sample.Models
{
[Obsolete("Use CurrentModel instead.")]
[Experimental("CUSTOM001")]
public partial class CustomCombinedModel
{
}
}
Loading
Loading