diff --git a/CHANGELOG.md b/CHANGELOG.md index 83fd111f37..fd961ed54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- PHP: include deprecation metadata in PHPDoc for generated classes, enums, properties and methods. - Ruby: models generated with `--backing-store` keep their values in the runtime's backing store and send only the values changed since they were read, as the other languages do. Before, `--backing-store` generated Ruby that failed to load. Requires the runtime gems at 0.25.0. [kiota-ruby#53](https://github.com/microsoft/kiota-ruby/issues/53) ### Changed diff --git a/src/Kiota.Builder/Writers/Php/CodeEnumWriter.cs b/src/Kiota.Builder/Writers/Php/CodeEnumWriter.cs index f8385dff69..33ab42d477 100644 --- a/src/Kiota.Builder/Writers/Php/CodeEnumWriter.cs +++ b/src/Kiota.Builder/Writers/Php/CodeEnumWriter.cs @@ -45,6 +45,7 @@ public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter write { writer.WriteLine(string.Empty); } + conventions.WriteLongDescription(codeElement, writer); writer.WriteLine($"class {codeElement?.Name.ToFirstCharacterUpperCase()} extends Enum {{"); writer.IncreaseIndent(); var reservedNames = enumProperties.Select(static option => GetEnumValueName(option.Name)).ToHashSet(StringComparer.Ordinal); diff --git a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs index 16529ea2ca..5353fb4075 100644 --- a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs @@ -329,7 +329,7 @@ private void WriteMethodPhpDocs(CodeMethod codeMethod, LanguageWriter writer) var methodDescription = codeMethod.Documentation.GetDescription(x => conventions.GetTypeString(x, codeMethod), normalizationFunc: PhpConventionService.RemoveInvalidDescriptionCharacters); var methodThrows = codeMethod.IsOfKind(CodeMethodKind.RequestExecutor); var hasMethodDescription = !string.IsNullOrEmpty(methodDescription.Trim()); - if (!hasMethodDescription && !codeMethod.Parameters.Any()) + if (!hasMethodDescription && !codeMethod.Parameters.Any() && codeMethod.Deprecation is not { IsDeprecated: true }) { return; } diff --git a/src/Kiota.Builder/Writers/Php/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/Php/CodePropertyWriter.cs index a9e151d1f7..d1409e7b02 100644 --- a/src/Kiota.Builder/Writers/Php/CodePropertyWriter.cs +++ b/src/Kiota.Builder/Writers/Php/CodePropertyWriter.cs @@ -47,6 +47,9 @@ private void WritePropertyDocComment(CodeProperty codeProperty, LanguageWriter w } writer.WriteLine($"{conventions.DocCommentPrefix}@var {typeString}{(codeProperty.Type.IsNullable ? "|null" : string.Empty)} ${codeProperty.Name.ToFirstCharacterLowerCase()} " + $"{(hasDescription ? propertyDescription : string.Empty)}"); + var deprecation = conventions.GetDeprecationDescription(codeProperty); + if (!string.IsNullOrEmpty(deprecation)) + writer.WriteLine($"{conventions.DocCommentPrefix}{deprecation}"); writer.WriteLine(PhpConventionService.DocCommentEnd); } diff --git a/src/Kiota.Builder/Writers/Php/PhpConventionService.cs b/src/Kiota.Builder/Writers/Php/PhpConventionService.cs index 687c4924c8..64ff295563 100644 --- a/src/Kiota.Builder/Writers/Php/PhpConventionService.cs +++ b/src/Kiota.Builder/Writers/Php/PhpConventionService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Kiota.Builder.CodeDOM; @@ -148,13 +149,17 @@ public override bool WriteShortDescription(IDocumentedElement element, LanguageW { ArgumentNullException.ThrowIfNull(writer); ArgumentNullException.ThrowIfNull(element); - if (!element.Documentation.DescriptionAvailable) return false; + var deprecation = GetDeprecationDescription(element); + if (!element.Documentation.DescriptionAvailable && string.IsNullOrEmpty(deprecation)) return false; if (element is not CodeElement codeElement) return false; var description = element.Documentation.GetDescription(type => GetTypeString(type, codeElement), normalizationFunc: RemoveInvalidDescriptionCharacters); writer.WriteLine(DocCommentStart); - writer.WriteLine($"{DocCommentPrefix}{description}"); + if (element.Documentation.DescriptionAvailable) + writer.WriteLine($"{DocCommentPrefix}{description}"); + if (!string.IsNullOrEmpty(deprecation)) + writer.WriteLine($"{DocCommentPrefix}{deprecation}"); writer.WriteLine(DocCommentEnd); return true; @@ -169,8 +174,9 @@ public void WriteLongDescription(IDocumentedElement element, LanguageWriter writ additionalRemarks ??= []; var enumerableArray = additionalRemarks as string[] ?? additionalRemarks.ToArray(); + var deprecation = GetDeprecationDescription(element); if (documentation.DescriptionAvailable || documentation.ExternalDocumentationAvailable || - enumerableArray.Length != 0) + enumerableArray.Length != 0 || !string.IsNullOrEmpty(deprecation)) { writer.WriteLine(DocCommentStart); if (documentation.DescriptionAvailable) @@ -178,6 +184,8 @@ public void WriteLongDescription(IDocumentedElement element, LanguageWriter writ var description = element.Documentation.GetDescription(type => GetTypeString(type, codeElement), normalizationFunc: RemoveInvalidDescriptionCharacters); writer.WriteLine($"{DocCommentPrefix}{description}"); } + if (!string.IsNullOrEmpty(deprecation)) + writer.WriteLine($"{DocCommentPrefix}{deprecation}"); foreach (var additionalRemark in enumerableArray.Where(static x => !string.IsNullOrEmpty(x))) writer.WriteLine($"{DocCommentPrefix}{additionalRemark}"); @@ -192,6 +200,18 @@ public void WriteLongDescription(IDocumentedElement element, LanguageWriter writ } + internal string GetDeprecationDescription(IDocumentedElement element) + { + if (element is not IDeprecableElement { Deprecation: { IsDeprecated: true } deprecation } || element is not CodeElement codeElement) + return string.Empty; + + var version = string.IsNullOrEmpty(deprecation.Version) ? string.Empty : $" {RemoveInvalidDescriptionCharacters(deprecation.Version.CleanupDescription())}"; + var description = deprecation.GetDescription(type => GetTypeString(type, codeElement), normalizationFunc: RemoveInvalidDescriptionCharacters); + var date = deprecation.Date is null ? string.Empty : $" on {deprecation.Date.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + var removal = deprecation.RemovalDate is null ? string.Empty : $" and will be removed {deprecation.RemovalDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + return $"@deprecated{version} {description}{date}{removal}".TrimEnd(); + } + public void AddRequestBuilderBody(string returnType, LanguageWriter writer, string? suffix = default, IEnumerable? pathParameters = default) { ArgumentNullException.ThrowIfNull(writer); diff --git a/tests/Kiota.Builder.Tests/Writers/Php/CodeEnumWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Php/CodeEnumWriterTests.cs index 0bbddb1681..a649831b31 100644 --- a/tests/Kiota.Builder.Tests/Writers/Php/CodeEnumWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Php/CodeEnumWriterTests.cs @@ -39,6 +39,29 @@ public void Dispose() tw?.Dispose(); GC.SuppressFinalize(this); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WritesDocumentationForCurrentEnums(bool explicitlyCurrent) + { + currentEnum.Documentation.DescriptionTemplate = "Available statuses */ \\next"; + currentEnum.Documentation.DocumentationLink = new Uri("https://example.com/status"); + currentEnum.Documentation.DocumentationLabel = "Status reference"; + if (explicitlyCurrent) + currentEnum.Deprecation = new DeprecationInformation("Current", IsDeprecated: false); + _codeEnumWriter.WriteCodeElement(currentEnum, writer); + var result = tw.ToString(); + Assert.Contains(" * Available statuses * / /next", result); + Assert.Contains(" * @link https://example.com/status Status reference", result); + Assert.DoesNotContain("Available statuses */", result); + Assert.DoesNotContain("@deprecated", result); + } + [Fact] + public void DoesNotWriteEmptyDocumentationForCurrentEnums() + { + _codeEnumWriter.WriteCodeElement(currentEnum, writer); + Assert.DoesNotContain("/**", tw.ToString()); + } [Fact] public void DisambiguatesEnumConstantsWithoutStealingExistingNames() { diff --git a/tests/Kiota.Builder.Tests/Writers/Php/DeprecationTests.cs b/tests/Kiota.Builder.Tests/Writers/Php/DeprecationTests.cs new file mode 100644 index 0000000000..253d848fe8 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Php/DeprecationTests.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Configuration; +using Kiota.Builder.Writers; + +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Php; + +public class DeprecationTests +{ + [Theory] + [InlineData("class")] + [InlineData("enum")] + [InlineData("property")] + [InlineData("navigation")] + [InlineData("method")] + public void WritesDeprecationWithoutDocumentation(string kind) + { + var result = WriteElement(kind, new DeprecationInformation("Use the replacement", Version: "2.0")); + Assert.Contains(" * @deprecated 2.0 Use the replacement", result); + Assert.Contains($" * @deprecated{Environment.NewLine}", WriteElement(kind, new DeprecationInformation(null))); + } + + [Theory] + [InlineData("class")] + [InlineData("enum")] + [InlineData("property")] + [InlineData("navigation")] + [InlineData("method")] + public void DoesNotDeprecateCurrentElements(string kind) + { + Assert.DoesNotContain("@deprecated", WriteElement(kind, null)); + Assert.DoesNotContain("@deprecated", WriteElement(kind, new DeprecationInformation("Current", IsDeprecated: false))); + } + + [Fact] + public void WritesDeprecationDatesAndNeutralizesCommentDelimiters() + { + var result = WriteElement("class", new DeprecationInformation( + "Use */ \"replacement\" /* '$value'\n\r\t\\next", new DateTimeOffset(2024, 1, 2, 0, 0, 0, TimeSpan.Zero), + new DateTimeOffset(2025, 3, 4, 0, 0, 0, TimeSpan.Zero), "2.0*/")); + Assert.Contains("@deprecated 2.0* /", result); + Assert.Contains("Use * /", result); + Assert.Contains("//*", result); + Assert.Contains("on 2024-01-02", result); + Assert.Contains("will be removed 2025-03-04", result); + Assert.DoesNotContain("Use */", result); + Assert.DoesNotContain("2.0*/", result); + } + + private static string WriteElement(string kind, DeprecationInformation deprecation) + { + var root = CodeNamespace.InitRootNamespace(); + root.Name = "Api"; + var parentClass = root.AddClass(new CodeClass { Name = "legacy", Deprecation = kind == "class" ? deprecation : null }).First(); + using var text = new StringWriter(); + var writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.PHP, "./", "client"); + writer.SetTextWriter(text); + switch (kind) + { + case "class": + writer.Write(parentClass.StartBlock); + break; + case "enum": + var codeEnum = root.AddEnum(new CodeEnum { Name = "status", Deprecation = deprecation }).First(); + codeEnum.AddOption(new CodeEnumOption { Name = "old" }); + writer.Write(codeEnum); + break; + case "method": + writer.Write(parentClass.AddMethod(new CodeMethod { Name = "legacyMethod", Deprecation = deprecation, ReturnType = new CodeType { Name = "void" } }).First()); + break; + default: + writer.Write(parentClass.AddProperty(new CodeProperty + { + Name = "legacyProperty", + Deprecation = deprecation, + Kind = kind == "navigation" ? CodePropertyKind.RequestBuilder : CodePropertyKind.Custom, + Type = new CodeType { Name = "string" } + }).First()); + break; + } + return text.ToString(); + } +}