-
Notifications
You must be signed in to change notification settings - Fork 0
build: track the public API surface with PublicApiAnalyzers #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| #nullable enable | ||
| E4A.PostGuard.Exceptions.NetworkException | ||
| E4A.PostGuard.Exceptions.NetworkException.Body.get -> string! | ||
| E4A.PostGuard.Exceptions.NetworkException.NetworkException(int statusCode, string! body, string! url) -> void | ||
| E4A.PostGuard.Exceptions.NetworkException.StatusCode.get -> int | ||
| E4A.PostGuard.Exceptions.NetworkException.Url.get -> string! | ||
| E4A.PostGuard.Exceptions.PostGuardException | ||
| E4A.PostGuard.Exceptions.PostGuardException.PostGuardException(string! message) -> void | ||
| E4A.PostGuard.Exceptions.PostGuardException.PostGuardException(string! message, System.Exception! inner) -> void | ||
| E4A.PostGuard.Exceptions.SealException | ||
| E4A.PostGuard.Exceptions.SealException.SealException(string! message) -> void | ||
| E4A.PostGuard.Models.ApiKeySign | ||
| E4A.PostGuard.Models.ApiKeySign.ApiKey.get -> string! | ||
| E4A.PostGuard.Models.EncryptInput | ||
| E4A.PostGuard.Models.EncryptInput.EncryptInput() -> void | ||
| E4A.PostGuard.Models.EncryptInput.Files.get -> System.Collections.Generic.IReadOnlyList<E4A.PostGuard.Models.PgFile!>! | ||
| E4A.PostGuard.Models.EncryptInput.Files.init -> void | ||
| E4A.PostGuard.Models.EncryptInput.Recipients.get -> System.Collections.Generic.IReadOnlyList<E4A.PostGuard.Models.RecipientBuilder!>! | ||
| E4A.PostGuard.Models.EncryptInput.Recipients.init -> void | ||
| E4A.PostGuard.Models.EncryptInput.Sign.get -> E4A.PostGuard.Models.SignMethod! | ||
| E4A.PostGuard.Models.EncryptInput.Sign.init -> void | ||
| E4A.PostGuard.Models.NotifyOptions | ||
| E4A.PostGuard.Models.NotifyOptions.Language.get -> string! | ||
| E4A.PostGuard.Models.NotifyOptions.Language.init -> void | ||
| E4A.PostGuard.Models.NotifyOptions.Message.get -> string? | ||
| E4A.PostGuard.Models.NotifyOptions.Message.init -> void | ||
| E4A.PostGuard.Models.NotifyOptions.NotifyOptions() -> void | ||
| E4A.PostGuard.Models.NotifyOptions.Recipients.get -> bool | ||
| E4A.PostGuard.Models.NotifyOptions.Recipients.init -> void | ||
| E4A.PostGuard.Models.NotifyOptions.Sender.get -> bool | ||
| E4A.PostGuard.Models.NotifyOptions.Sender.init -> void | ||
| E4A.PostGuard.Models.PgFile | ||
| E4A.PostGuard.Models.PgFile.Content.get -> System.IO.Stream! | ||
| E4A.PostGuard.Models.PgFile.Content.init -> void | ||
| E4A.PostGuard.Models.PgFile.Deconstruct(out string! Name, out System.IO.Stream! Content) -> void | ||
| E4A.PostGuard.Models.PgFile.Name.get -> string! | ||
| E4A.PostGuard.Models.PgFile.Name.init -> void | ||
| E4A.PostGuard.Models.PgFile.PgFile(E4A.PostGuard.Models.PgFile! original) -> void | ||
| E4A.PostGuard.Models.PgFile.PgFile(string! Name, System.IO.Stream! Content) -> void | ||
| E4A.PostGuard.Models.RecipientBuilder | ||
| E4A.PostGuard.Models.RecipientBuilder.Email.get -> string! | ||
| E4A.PostGuard.Models.RecipientBuilder.ExtraAttribute(string! type, string! value) -> E4A.PostGuard.Models.RecipientBuilder! | ||
| E4A.PostGuard.Models.SignMethod | ||
| E4A.PostGuard.Models.UploadOptions | ||
| E4A.PostGuard.Models.UploadOptions.Notify.get -> E4A.PostGuard.Models.NotifyOptions? | ||
| E4A.PostGuard.Models.UploadOptions.Notify.init -> void | ||
| E4A.PostGuard.Models.UploadOptions.UploadOptions() -> void | ||
| E4A.PostGuard.Models.UploadResult | ||
| E4A.PostGuard.Models.UploadResult.Deconstruct(out string! Uuid) -> void | ||
| E4A.PostGuard.Models.UploadResult.UploadResult(E4A.PostGuard.Models.UploadResult! original) -> void | ||
| E4A.PostGuard.Models.UploadResult.UploadResult(string! Uuid) -> void | ||
| E4A.PostGuard.Models.UploadResult.Uuid.get -> string! | ||
| E4A.PostGuard.Models.UploadResult.Uuid.init -> void | ||
| E4A.PostGuard.PostGuard | ||
| E4A.PostGuard.PostGuard.Dispose() -> void | ||
| E4A.PostGuard.PostGuard.Encrypt(E4A.PostGuard.Models.EncryptInput! input) -> E4A.PostGuard.Sealed! | ||
| E4A.PostGuard.PostGuard.PostGuard(E4A.PostGuard.PostGuardConfig! config) -> void | ||
| E4A.PostGuard.PostGuard.Recipient.get -> E4A.PostGuard.RecipientBuilders! | ||
| E4A.PostGuard.PostGuard.Sign.get -> E4A.PostGuard.SignBuilders! | ||
| E4A.PostGuard.PostGuardConfig | ||
| E4A.PostGuard.PostGuardConfig.AllowInsecureUrls.get -> bool | ||
| E4A.PostGuard.PostGuardConfig.AllowInsecureUrls.init -> void | ||
| E4A.PostGuard.PostGuardConfig.CryptifyUrl.get -> string! | ||
| E4A.PostGuard.PostGuardConfig.CryptifyUrl.init -> void | ||
| E4A.PostGuard.PostGuardConfig.Headers.get -> System.Collections.Generic.Dictionary<string!, string!>? | ||
| E4A.PostGuard.PostGuardConfig.Headers.init -> void | ||
| E4A.PostGuard.PostGuardConfig.HttpClient.get -> System.Net.Http.HttpClient? | ||
| E4A.PostGuard.PostGuardConfig.HttpClient.init -> void | ||
| E4A.PostGuard.PostGuardConfig.PkgUrl.get -> string! | ||
| E4A.PostGuard.PostGuardConfig.PkgUrl.init -> void | ||
| E4A.PostGuard.PostGuardConfig.PostGuardConfig() -> void | ||
| E4A.PostGuard.PostGuardConfig.Timeout.get -> System.TimeSpan? | ||
| E4A.PostGuard.PostGuardConfig.Timeout.init -> void | ||
| E4A.PostGuard.RecipientBuilders | ||
| E4A.PostGuard.RecipientBuilders.Email(string! email) -> E4A.PostGuard.Models.RecipientBuilder! | ||
| E4A.PostGuard.RecipientBuilders.EmailDomain(string! email) -> E4A.PostGuard.Models.RecipientBuilder! | ||
| E4A.PostGuard.RecipientBuilders.RecipientBuilders() -> void | ||
| E4A.PostGuard.Sealed | ||
| E4A.PostGuard.Sealed.ToBytesAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<byte[]!>! | ||
| E4A.PostGuard.Sealed.UploadAsync(E4A.PostGuard.Models.UploadOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<E4A.PostGuard.Models.UploadResult!>! | ||
| E4A.PostGuard.SignBuilders | ||
| E4A.PostGuard.SignBuilders.ApiKey(string! apiKey) -> E4A.PostGuard.Models.ApiKeySign! | ||
| E4A.PostGuard.SignBuilders.SignBuilders() -> void | ||
| override E4A.PostGuard.Models.PgFile.Equals(object? obj) -> bool | ||
| override E4A.PostGuard.Models.PgFile.GetHashCode() -> int | ||
| override E4A.PostGuard.Models.PgFile.ToString() -> string! | ||
| override E4A.PostGuard.Models.UploadResult.Equals(object? obj) -> bool | ||
| override E4A.PostGuard.Models.UploadResult.GetHashCode() -> int | ||
| override E4A.PostGuard.Models.UploadResult.ToString() -> string! | ||
| static E4A.PostGuard.Models.PgFile.operator !=(E4A.PostGuard.Models.PgFile? left, E4A.PostGuard.Models.PgFile? right) -> bool | ||
| static E4A.PostGuard.Models.PgFile.operator ==(E4A.PostGuard.Models.PgFile? left, E4A.PostGuard.Models.PgFile? right) -> bool | ||
| static E4A.PostGuard.Models.UploadResult.operator !=(E4A.PostGuard.Models.UploadResult? left, E4A.PostGuard.Models.UploadResult? right) -> bool | ||
| static E4A.PostGuard.Models.UploadResult.operator ==(E4A.PostGuard.Models.UploadResult? left, E4A.PostGuard.Models.UploadResult? right) -> bool | ||
| virtual E4A.PostGuard.Models.PgFile.<Clone>$() -> E4A.PostGuard.Models.PgFile! | ||
| virtual E4A.PostGuard.Models.PgFile.EqualityContract.get -> System.Type! | ||
| virtual E4A.PostGuard.Models.PgFile.Equals(E4A.PostGuard.Models.PgFile? other) -> bool | ||
| virtual E4A.PostGuard.Models.PgFile.PrintMembers(System.Text.StringBuilder! builder) -> bool | ||
| virtual E4A.PostGuard.Models.UploadResult.<Clone>$() -> E4A.PostGuard.Models.UploadResult! | ||
| virtual E4A.PostGuard.Models.UploadResult.EqualityContract.get -> System.Type! | ||
| virtual E4A.PostGuard.Models.UploadResult.Equals(E4A.PostGuard.Models.UploadResult? other) -> bool | ||
| virtual E4A.PostGuard.Models.UploadResult.PrintMembers(System.Text.StringBuilder! builder) -> bool |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| #nullable enable | ||
| *REMOVED*E4A.PostGuard.Exceptions.NetworkException.Body.get -> string! | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| using System.Reflection; | ||
| using System.Xml.Linq; | ||
|
|
||
| namespace E4A.PostGuard.Tests; | ||
|
|
||
| /// <summary> | ||
| /// The public surface is tracked in <c>src/PublicAPI.Shipped.txt</c> and | ||
| /// <c>src/PublicAPI.Unshipped.txt</c> and guarded by Microsoft.CodeAnalysis.PublicApiAnalyzers. | ||
| /// The analyzer only guards what it is wired up to see, so these tests cover the wiring itself | ||
| /// and check the tracked types against the assembly that ships. | ||
| /// </summary> | ||
| public class PublicApiTrackingTests | ||
| { | ||
| [Fact] | ||
| public void EveryExportedType_IsDeclaredInThePublicApiFiles() | ||
| { | ||
| var declared = DeclaredEntries(); | ||
| var declaredTypes = declared.Where(entry => !entry.Contains("->")).ToHashSet(StringComparer.Ordinal); | ||
|
|
||
| var exported = typeof(PostGuard).Assembly.GetExportedTypes() | ||
| .Select(DeclaredName) | ||
| .ToHashSet(StringComparer.Ordinal); | ||
|
|
||
| var undeclared = exported.Except(declaredTypes).Order(StringComparer.Ordinal).ToArray(); | ||
| var stale = declaredTypes.Except(exported).Order(StringComparer.Ordinal).ToArray(); | ||
|
|
||
| Assert.True( | ||
| undeclared.Length == 0, | ||
| $"public types missing from the PublicAPI files: {string.Join(", ", undeclared)}"); | ||
| Assert.True( | ||
| stale.Length == 0, | ||
| $"PublicAPI files declare types the assembly no longer exports: {string.Join(", ", stale)}"); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("PublicAPI.Shipped.txt")] | ||
| [InlineData("PublicAPI.Unshipped.txt")] | ||
| public void ApiFile_TracksNullability(string fileName) | ||
| { | ||
| // Without the header the analyzer records every reference type as oblivious, so a | ||
| // nullable-annotation change would slip through unreviewed. Dropping it from the | ||
| // unshipped file still builds green, so nothing but this test catches that. | ||
| var lines = File.ReadAllLines(Path.Combine(RepoRoot(), "src", fileName)); | ||
| Assert.Equal("#nullable enable", lines.FirstOrDefault()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Csproj_KeepsTheAnalyzerWiredUpAndFailingTheBuild() | ||
| { | ||
| // Parsed rather than substring-matched: reformatting an element, or moving | ||
| // WarningsAsErrors into a second PropertyGroup, leaves the wiring fully intact | ||
| // and must not red-light this test. | ||
| var csproj = XDocument.Load(Path.Combine(RepoRoot(), "src", "E4A.PostGuard.csproj")); | ||
|
|
||
| Assert.Contains("Microsoft.CodeAnalysis.PublicApiAnalyzers", Includes(csproj, "PackageReference")); | ||
| Assert.Contains("PublicAPI.Shipped.txt", Includes(csproj, "AdditionalFiles")); | ||
| Assert.Contains("PublicAPI.Unshipped.txt", Includes(csproj, "AdditionalFiles")); | ||
|
|
||
| // Left as warnings, an undeclared API change scrolls past in a green build. | ||
| var warningsAsErrors = string.Join(';', csproj.Descendants("WarningsAsErrors").Select(element => element.Value)); | ||
| Assert.Contains("RS0016", warningsAsErrors, StringComparison.Ordinal); | ||
| Assert.Contains("RS0017", warningsAsErrors, StringComparison.Ordinal); | ||
| } | ||
|
|
||
| /// <summary>The <c>Include</c> attribute of every <paramref name="elementName"/> item in the project.</summary> | ||
| private static string[] Includes(XDocument csproj, string elementName) => | ||
| csproj.Descendants(elementName) | ||
| .Select(element => (string?)element.Attribute("Include")) | ||
| .OfType<string>() | ||
| .ToArray(); | ||
|
|
||
| /// <summary>Shipped plus unshipped additions, minus the entries marked <c>*REMOVED*</c>.</summary> | ||
| private static HashSet<string> DeclaredEntries() | ||
| { | ||
| const string removedPrefix = "*REMOVED*"; | ||
| var root = Path.Combine(RepoRoot(), "src"); | ||
|
|
||
| var entries = new[] { "PublicAPI.Shipped.txt", "PublicAPI.Unshipped.txt" } | ||
| .SelectMany(name => File.ReadAllLines(Path.Combine(root, name))) | ||
| .Select(line => line.Trim()) | ||
| .Where(line => line.Length > 0 && !line.StartsWith('#')) | ||
| .ToArray(); | ||
|
|
||
| var declared = entries | ||
| .Where(line => !line.StartsWith(removedPrefix, StringComparison.Ordinal)) | ||
| .ToHashSet(StringComparer.Ordinal); | ||
|
|
||
| foreach (var removed in entries.Where(line => line.StartsWith(removedPrefix, StringComparison.Ordinal))) | ||
| { | ||
| declared.Remove(removed[removedPrefix.Length..]); | ||
| } | ||
|
|
||
| return declared; | ||
| } | ||
|
|
||
| /// <summary>The name a type is written under in the PublicAPI files.</summary> | ||
| private static string DeclaredName(Type type) | ||
| { | ||
| var name = type.FullName!.Replace('+', '.'); | ||
| var arity = name.IndexOf('`'); | ||
| return arity < 0 | ||
| ? name | ||
| : $"{name[..arity]}<{string.Join(", ", type.GetGenericArguments().Select(argument => argument.Name))}>"; | ||
| } | ||
|
|
||
| private static string RepoRoot() | ||
| { | ||
| var dir = new DirectoryInfo(AppContext.BaseDirectory); | ||
| while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "E4A.PostGuard.slnx"))) | ||
| { | ||
| dir = dir.Parent; | ||
| } | ||
|
|
||
| Assert.NotNull(dir); | ||
| return dir!.FullName; | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not introduced by this PR, and no change needed here, but this file is what makes it visible.
The
*REMOVED*entry formally records that the pending release drops a public property. Release PR #38 is open and cutsE4A.PostGuard 0.5.1, a patch bump, because #43 landed asfix:. Publishing a patch that removes a public member breaks consumers ondotnet restorewith nothing in the version number to signal it.Worth deciding before #38 merges: re-cut as 0.6.0, or accept it as pre-1.0 latitude.