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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
Expand Down Expand Up @@ -3315,6 +3316,69 @@ public async Task BackCompatibility_NewOptionalNonBodyParameterAdded()
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
Comment thread
JoshLove-msft marked this conversation as resolved.
}

[Test]
public async Task BackCompatibility_CurrentDocumentationSurvivesClientProcessing()
{
var operation = InputFactory.Operation(
"GetData",
parameters:
[
InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true),
InputFactory.BodyParameter("param2", InputPrimitiveType.String, isRequired: true),
InputFactory.HeaderParameter("param3", InputPrimitiveType.Boolean)
],
responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]);
var method = InputFactory.BasicServiceMethod("GetData", operation, parameters:
[
InputFactory.MethodParameter("param1", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true),
InputFactory.MethodParameter("param2", InputPrimitiveType.String, location: InputRequestLocation.Body, isRequired: true),
InputFactory.MethodParameter("param3", InputPrimitiveType.Boolean, location: InputRequestLocation.Header)
]);
var client = InputFactory.Client(TestClientName, methods: [method]);
var generator = await MockHelpers.LoadMockGeneratorAsync(
clients: () => [client],
lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync(method: nameof(BackCompatibility_NewOptionalNonBodyParameterAdded)),
configuration: """{"disable-xml-docs": false}""");
var provider = generator.Object.OutputLibrary.TypeProviders.OfType<ClientProvider>().Single();
var originalMethods = provider.Methods.ToArray();
foreach (var original in originalMethods.Where(m => m.Signature.Name is "GetData" or "GetDataAsync"))
{
original.XmlDocs.Update(summary: new XmlDocSummaryStatement([$"Current operation."],
new XmlDocStatement("list", [], new XmlDocStatement("item", [$"Current metadata."]))));
}
var originalDocs = originalMethods.ToDictionary(m => m, RenderDocs);

provider.ProcessTypeForBackCompatibility();

var shims = provider.Methods.Except(originalMethods).ToArray();
Assert.AreEqual(2, shims.Length);
foreach (var shim in shims)
{
string rendered = RenderDocs(shim);
var docs = XElement.Parse("<member>" + string.Join("\n", rendered.Split('\n')
.Where(line => line.StartsWith("///")).Select(line => line[3..])) + "</member>");
Assert.IsNotNull(docs.Element("summary")?.Element("list"));
StringAssert.Contains("Current operation.", rendered);
Assert.AreEqual(new[] { "param1", "param2", "cancellationToken" },
docs.Elements("param").Select(p => (string?)p.Attribute("name")));
Assert.IsTrue(docs.Descendants("paramref").All(p => (string?)p.Attribute("name") != "param3"));
Assert.AreEqual(1, shim.Suppressions.Count);
}
foreach (var original in originalMethods)
{
Assert.AreEqual(originalDocs[original], RenderDocs(original));
}

static string RenderDocs(MethodProvider method)
{
using var writer = new CodeWriter();
using (writer.WriteXmlDocs(method.XmlDocs))
{
return writer.ToString(false);
}
}
}

// The current TypeSpec adds two new optional non-body parameters relative to the last contract.
// Expected: a single back-compat overload matching the previous signature is added that
// delegates to the new method, passing default for both new parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public XmlDocExceptionStatement(Type exceptionType, string reason, IReadOnlyList
_reason = reason;
}

internal XmlDocExceptionStatement WithParameters(IReadOnlyList<ParameterProvider> parameters)
=> new XmlDocExceptionStatement(ExceptionType, _reason, parameters);

private static string GetText(Type exceptionType) => exceptionType switch
{
{ } when exceptionType == typeof(ArgumentNullException) => "is null.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Snippets;

Expand All @@ -9,7 +11,12 @@ namespace Microsoft.TypeSpec.Generator.Statements
public sealed class XmlDocParamStatement : XmlDocStatement
{
public XmlDocParamStatement(ParameterProvider parameter)
: base($"<param name=\"{parameter.AsVariable().Declaration}\">", $"</param>", [parameter.Description])
: this(parameter, [parameter.Description])
{
}

internal XmlDocParamStatement(ParameterProvider parameter, IEnumerable<FormattableString> lines, params XmlDocStatement[] innerStatements)
: base($"<param name=\"{parameter.AsVariable().Declaration}\">", $"</param>", lines, innerStatements)
{
Parameter = parameter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ private static MethodProvider BuildNewOptionalParameterOverload(
signature,
body,
enclosingType,
previousMethod.XmlDocs);
BuildOverloadXmlDocs(currentMethod, signature, enclosingType));
}

// Forwards a previous parameter to the current method. When the parameter's value-type nullability
Expand Down Expand Up @@ -859,7 +859,7 @@ private static MethodProvider BuildChangedParameterNullabilityOverload(
signature,
body,
enclosingType,
previousMethod.XmlDocs);
BuildOverloadXmlDocs(currentMethod, signature, enclosingType));
}

// Given two signatures already known to have equal parameter count and types (including nullability),
Expand Down Expand Up @@ -956,45 +956,75 @@ private static MethodProvider BuildOptionalityRestorationOverload(

var body = BuildDelegatingBody(enclosingType, currentSignature, arguments);
var signature = BuildHiddenOverloadSignature(previousSignature, shimParameters);
var xmlDocs = BuildXmlDocsWithoutParameter(previousMethod.XmlDocs, droppedParameter.Name.ToVariableName());

return new MethodProvider(
signature,
body,
enclosingType,
xmlDocs);
BuildOverloadXmlDocs(currentMethod, signature, enclosingType));
}

// Rebuilds an XML doc provider without any reference to the dropped parameter: its &lt;param&gt;
// entry is removed and every &lt;exception&gt; that referenced it is rebuilt without that
// &lt;paramref&gt; (exceptions that referenced only the dropped parameter are removed entirely).
// This avoids stale-doc compile errors (CS1572/CS1734) on the reduced-arity overload.
private static XmlDocProvider BuildXmlDocsWithoutParameter(XmlDocProvider docs, string droppedVariableName)
// Released XML has already been flattened by NamedTypeSymbolProvider. Use the current
// documentation, binding parameter tags/references to the shim without mutating either method.
private static XmlDocProvider BuildOverloadXmlDocs(
MethodProvider currentMethod,
MethodSignature signature,
TypeProvider enclosingType)
{
var filteredParameters = docs.Parameters
.Where(p => p.Parameter.Name.ToVariableName() != droppedVariableName)
.ToList();
var docs = currentMethod.XmlDocs;
var parametersByName = signature.Parameters.ToDictionary(p => p.Name.ToVariableName());
var parameterDocsByName = docs.Parameters.ToDictionary(p => p.Parameter.Name.ToVariableName());
var parameters = new List<XmlDocParamStatement>();
foreach (var parameter in signature.Parameters)
{
if (parameterDocsByName.TryGetValue(parameter.Name.ToVariableName(), out var parameterDoc))
{
parameters.Add(new XmlDocParamStatement(parameter, parameterDoc.Lines, [.. parameterDoc.InnerStatements]));
}
}

var filteredExceptions = new List<XmlDocExceptionStatement>(docs.Exceptions.Count);
var exceptions = new List<XmlDocExceptionStatement>();
foreach (var exceptionDoc in docs.Exceptions)
{
var remaining = exceptionDoc.Parameters
.Where(p => p.Name.ToVariableName() != droppedVariableName)
.ToList();
.Select(p => parametersByName.GetValueOrDefault(p.Name.ToVariableName()))
.OfType<ParameterProvider>()
.ToArray();

// Drop an exception that referenced only the removed parameter; keep an unrelated one as-is;
// otherwise rebuild it without the removed paramref.
if (exceptionDoc.Parameters.Count > 0 && remaining.Count == 0)
if (exceptionDoc.Parameters.Count > 0 && remaining.Length == 0)
{
continue;
}

filteredExceptions.Add(remaining.Count == exceptionDoc.Parameters.Count
? exceptionDoc
: new XmlDocExceptionStatement(exceptionDoc.ExceptionType, remaining));
exceptions.Add(exceptionDoc.WithParameters(remaining));
}

// The shim can validate more than its target, notably before unwrapping T? to T.
foreach (var exceptionDoc in MethodProviderHelpers.BuildXmlDocs(signature, enclosingType).Exceptions)
{
AddValidationException(exceptionDoc);
}
var currentParametersByName = currentMethod.Signature.Parameters.ToDictionary(p => p.Name.ToVariableName());
var unwrappedParameters = signature.Parameters.Where(p =>
currentParametersByName.TryGetValue(p.Name.ToVariableName(), out var currentParameter)
&& IsNullabilityRelaxedValueType(p.Type, currentParameter.Type)).ToArray();
if (unwrappedParameters.Length > 0)
{
AddValidationException(new XmlDocExceptionStatement(typeof(ArgumentNullException), unwrappedParameters));
}

return new XmlDocProvider(docs.Summary, filteredParameters, filteredExceptions, docs.Returns, docs.Inherit);
return new XmlDocProvider(docs.Summary, parameters, exceptions, docs.Returns, docs.Inherit);

void AddValidationException(XmlDocExceptionStatement exceptionDoc)
{
var documentedParameters = exceptions.Where(e => e.ExceptionType == exceptionDoc.ExceptionType)
.SelectMany(e => e.Parameters).ToHashSet();
var missingParameters = exceptionDoc.Parameters.Where(p => !documentedParameters.Contains(p)).ToArray();
if (missingParameters.Length > 0)
{
exceptions.Add(exceptionDoc.WithParameters(missingParameters));
}
}
}
}
}
Loading
Loading