diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 50eb5f98..6e733e45 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -34,4 +34,4 @@ Ensure you have replicated the bug in a minimal solution with the fewest moving #### Submit a PR that fixes the bug -Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice. \ No newline at end of file +Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice. diff --git a/.github/workflows/merge-dependabot.yml b/.github/workflows/merge-dependabot.yml index a6902544..6715a04f 100644 --- a/.github/workflows/merge-dependabot.yml +++ b/.github/workflows/merge-dependabot.yml @@ -7,7 +7,7 @@ jobs: if: github.actor == 'dependabot[bot]' steps: - name: Dependabot Auto Merge - uses: ahmadnassri/action-dependabot-auto-merge@v2.3.1 + uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6 with: target: minor github-token: ${{ secrets.dependabot }} diff --git a/.github/workflows/on-tag-do-release.yml b/.github/workflows/on-tag-do-release.yml deleted file mode 100644 index f8422955..00000000 --- a/.github/workflows/on-tag-do-release.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: on-tag-do-release -on: - push: - tags: - - '[0-9]+\.[0-9]+\.[0-9]+' -jobs: - release: - runs-on: windows-latest - steps: - - name: Run - run: | - dotnet tool install --global GitReleaseManager.Tool - tag="${GITHUB_REF:10}" - owner="${GITHUB_REPOSITORY%/*}" - repo="${GITHUB_REPOSITORY#*/}" - dotnet-gitreleasemanager create -m ${tag} --token ${{secrets.GITHUB_TOKEN}} -o ${owner} -r ${repo} - dotnet-gitreleasemanager close -m ${tag} --token ${{secrets.GITHUB_TOKEN}} -o ${owner} -r ${repo} - dotnet-gitreleasemanager publish -t ${tag} --token ${{secrets.GITHUB_TOKEN}} -o ${owner} -r ${repo} - shell: bash \ No newline at end of file diff --git a/api_list.include.md b/api_list.include.md new file mode 100644 index 00000000..98b58ef1 --- /dev/null +++ b/api_list.include.md @@ -0,0 +1,309 @@ +### Extension methods + +#### Boolean + + * `Boolean TryFormat(Span, Int32&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.boolean.tryformat) + + +#### Byte + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.byte.tryformat) + + +#### Dictionary + + * `Boolean Remove(TKey, TValue&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.remove) + + +#### IEnumerable + + * `IEnumerable Chunk(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk) + * `IEnumerable Except(TSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except?view=net-8.0#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0)))) + * `IEnumerable Except(TSource[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except?view=net-8.0#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0)))) + * `IEnumerable Except(TSource, IEqualityComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))) + * `IEnumerable Except(IEqualityComparer, TSource[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))) + * `TSource MaxBy(Func)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))) + * `TSource MaxBy(Func, IComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))) + * `TSource MinBy(Func)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))) + * `TSource MinBy(Func, IComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby?view=net-8.0#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))) + * `IEnumerable SkipLast(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast) + * `HashSet ToHashSet(IEqualityComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tohashset#system-linq-enumerable-tohashset-1(system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))) + + +#### IReadOnlyDictionary + + * `TValue GetValueOrDefault(TKey)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault) + * `TValue GetValueOrDefault(TKey, TValue)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault#system-collections-generic-collectionextensions-getvalueordefault-2(system-collections-generic-ireadonlydictionary((-0-1))-0-1)) + + +#### KeyValuePair + + * `Void Deconstruct(TKey&, TValue&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.keyvaluepair-2.deconstruct) + + +#### DateTime + + * `DateTime AddMicroseconds(Double)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.addmicroseconds) + * `Int32 Microsecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.microsecond) + * `Int32 Nanosecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.nanosecond) + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.tryformat) + + +#### DateTimeOffset + + * `DateTimeOffset AddMicroseconds(Double)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.addmicroseconds) + * `Int32 Microsecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.microsecond) + * `Int32 Nanosecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.nanosecond) + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tryformat) + + +#### Decimal + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.decimal.tryformat) + + +#### Process + + * `Task WaitForExitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync) + + +#### Double + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.double.tryformat) + + +#### Guid + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.guid.tryformat#system-guid-tryformat(system-span((system-char))-system-int32@-system-readonlyspan((system-char)))) + + +#### Int16 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int16.tryformat) + + +#### Int32 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int32.tryformat) + + +#### Int64 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int64.tryformat) + + +#### Stream + + * `Task CopyToAsync(Stream, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.copytoasync#system-io-stream-copytoasync(system-io-stream-system-threading-cancellationtoken)) + * `ValueTask ReadAsync(Memory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.readasync#system-io-stream-readasync(system-memory((system-byte))-system-threading-cancellationtoken)) + * `ValueTask WriteAsync(ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.writeasync#system-io-stream-writeasync(system-readonlymemory((system-byte))-system-threading-cancellationtoken)) + + +#### TextReader + + * `ValueTask ReadAsync(Memory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readasync#system-io-textreader-readasync(system-memory((system-char))-system-threading-cancellationtoken)) + * `Task ReadLineAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readlineasync(system-threading-cancellationtoken)) + * `Task ReadToEndAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readtoendasync(system-threading-cancellationtoken)) + + +#### TextWriter + + * `Void Write(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))) + * `ValueTask WriteAsync(ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)) + * `Void WriteLine(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeline#system-io-textwriter-writeline(system-readonlyspan((system-char)))) + * `ValueTask WriteLineAsync(ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync#system-io-textwriter-writelineasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)) + + +#### HttpClient + + * `Task GetByteArrayAsync(String, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync#system-net-http-httpclient-getbytearrayasync(system-string-system-threading-cancellationtoken)) + * `Task GetByteArrayAsync(Uri, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync#system-net-http-httpclient-getbytearrayasync(system-uri-system-threading-cancellationtoken)) + * `Task GetStreamAsync(String, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstreamasync#system-net-http-httpclient-getstreamasync(system-string-system-threading-cancellationtoken)) + * `Task GetStreamAsync(Uri, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstreamasync#system-net-http-httpclient-getstreamasync(system-uri-system-threading-cancellationtoken)) + * `Task GetStringAsync(String, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstringasync#system-net-http-httpclient-getstringasync(system-string-system-threading-cancellationtoken)) + * `Task GetStringAsync(Uri, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstringasync#system-net-http-httpclient-getstringasync(system-uri-system-threading-cancellationtoken)) + + +#### HttpContent + + * `Task ReadAsByteArrayAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasbytearrayasync#system-net-http-httpcontent-readasbytearrayasync(system-threading-cancellationtoken)) + * `Task ReadAsStreamAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasstreamasync#system-net-http-httpcontent-readasstreamasync(system-threading-cancellationtoken)) + * `Task ReadAsStringAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasstringasync#system-net-http-httpcontent-readasstringasync(system-threading-cancellationtoken)) + + +#### ReadOnlySpan + + * `Boolean EndsWith(String, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.endswith#system-memoryextensions-endswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + * `Boolean SequenceEqual(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + * `Boolean StartsWith(String, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith#system-memoryextensions-startswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + + +#### ReadOnlySpan + + * `Boolean Contains(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.contains#system-memoryextensions-contains-1(system-readonlyspan((-0))-0)) + + +#### Reflection.EventInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` + + +#### Reflection.FieldInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` + + +#### Reflection.MemberInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean HasSameMetadataDefinitionAs(Reflection.MemberInfo)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.hassamemetadatadefinitionas) + * `Boolean IsNullable()` + + +#### Reflection.ParameterInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` + + +#### Reflection.PropertyInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` + + +#### SByte + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.sbyte.tryformat) + + +#### Single + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.single.tryformat) + + +#### Span + + * `Boolean EndsWith(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.endswith#system-memoryextensions-endswith-1(system-span((-0))-system-readonlyspan((-0)))) + * `Boolean SequenceEqual(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-span((-0))-system-readonlyspan((-0)))) + * `Boolean StartsWith(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith#system-memoryextensions-startswith-1(system-span((-0))-system-readonlyspan((-0)))) + + +#### Span + + * `Boolean Contains(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.contains#system-memoryextensions-contains-1(system-span((-0))-0)) + + +#### String + + * `Boolean Contains(String, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-string-system-stringcomparison)) + * `Boolean Contains(Char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)) + * `Void CopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.copyto) + * `Boolean EndsWith(Char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)) + * `Int32 GetHashCode(StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.gethashcode#system-string-gethashcode(system-stringcomparison)) + * `String[] Split(Char, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.split#system-string-split(system-char-system-stringsplitoptions)) + * `String[] Split(Char, Int32, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.split#system-string-split(system-char-system-int32-system-stringsplitoptions)) + * `Boolean StartsWith(Char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)) + * `Boolean TryCopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.trycopyto) + + +#### Regex + + * `ValueMatchEnumerator EnumerateMatches(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char)))) + * `ValueMatchEnumerator EnumerateMatches(ReadOnlySpan, Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char))-system-int32)) + * `Boolean IsMatch(ReadOnlySpan, Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-int32)) + * `Boolean IsMatch(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char)))) + + +#### StringBuilder + + * `StringBuilder Append(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-readonlyspan((system-char)))) + * `StringBuilder Append(StringBuilder, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `StringBuilder Append(StringBuilder, IFormatProvider, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `StringBuilder AppendJoin(String, String[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-string-system-string())) + * `StringBuilder AppendJoin(String, Object[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-string-system-object())) + * `StringBuilder AppendJoin(Char, String[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-char-system-string())) + * `StringBuilder AppendJoin(Char, Object[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-char-system-object())) + * `StringBuilder AppendJoin(Char, T[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin-1(system-char-system-collections-generic-ienumerable((-0)))) + * `StringBuilder AppendJoin(String, T[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin-1(system-string-system-collections-generic-ienumerable((-0)))) + * `StringBuilder AppendLine(StringBuilder, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `StringBuilder AppendLine(StringBuilder, IFormatProvider, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `Void CopyTo(Int32, Span, Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.copyto#system-text-stringbuilder-copyto(system-int32-system-span((system-char))-system-int32)) + * `Boolean Equals(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.equals#system-text-stringbuilder-equals(system-readonlyspan((system-char)))) + + +#### CancellationToken + + * `CancellationTokenRegistration Register(Action, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.register#system-threading-cancellationtoken-register(system-action((system-object-system-threading-cancellationtoken))-system-object)) + * `CancellationTokenRegistration UnsafeRegister(Action, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object))-system-object)) + * `CancellationTokenRegistration UnsafeRegister(Action, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object-system-threading-cancellationtoken))-system-object)) + + +#### CancellationTokenSource + + * `Task CancelAsync()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync) + + +#### Task + + * `Task WaitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-threading-cancellationtoken)) + * `Task WaitAsync(TimeSpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-timespan)) + * `Task WaitAsync(TimeSpan, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-timespan-system-threading-cancellationtoken)) + + +#### Task + + * `Task WaitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-threading-cancellationtoken)) + * `Task WaitAsync(TimeSpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.waitasync#system-threading-tasks-task-1-waitasync(system-timespan)) + * `Task WaitAsync(TimeSpan, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.waitasync#system-threading-tasks-task-1-waitasync(system-timespan-system-threading-cancellationtoken)) + + +#### TimeSpan + + * `Int32 Microseconds()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.microseconds) + * `Int32 Nanoseconds()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.nanoseconds) + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.tryformat#system-timespan-tryformat(system-span((system-byte))-system-int32@-system-readonlyspan((system-char))-system-iformatprovider)) + + +#### Type + + * `Boolean IsGenericMethodParameter()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.type.isgenericmethodparameter) + + +#### UInt16 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uint16.tryformat) + + +#### UInt32 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uint32.tryformat) + + +#### UInt64 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uint64.tryformat) + + +### Static helpers + +#### EnumPolyfill + + * `String[] GetNames()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.enum.getnames) + * `TEnum[] GetValues()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.enum.getvalues) + + +#### RegexPolyfill + + * `Boolean IsMatch(ReadOnlySpan, String, RegexOptions, TimeSpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions-system-timespan)) + * `Boolean IsMatch(ReadOnlySpan, String, RegexOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions)) + * `Boolean IsMatch(ReadOnlySpan, String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string)) + + diff --git a/contributing.md b/contributing.md new file mode 100644 index 00000000..54dead81 --- /dev/null +++ b/contributing.md @@ -0,0 +1,360 @@ +# Contributing + + +## Solution Structure + + +### Polyfill project + +The main project that produces the nuget. + + +### Tests project + +A NUnit test project that verifies all the APIs. + + +### NoRefsTests project + +Some features of Polyfill [require nuget references](/#references) to be enabled. The NoRefsTests project had none of those refecences and tests the subset of features that do not require references. + + +### PublicTests project + +Polyfill supports [making all APIs public](#consuming-and-type-visibility). The PublicTests project tests that scenario. + + +### UnsafeTests project + +Some feature of Polyfill leverage unsafe code for better performance. For example `Append(this StringBuilder, ReadOnlySpan)`. The UnsafeTests project tests this scenario vie enabling `True`. + + +### Consume project + +Polyfill supports back to `net461` and `netcoreapp2.0`. However NUnit only support back to `net462` and `netcoreapp3.1`. The Consume project targets all frameworks that Polyfill supports, and consumes all APIs to ensure that they all compile on those frameworks + + +### ConsumeClassicReferences Project + +Test the scenario when references are added through ` + +```cs +// + +#if !NET5_0_OR_GREATER + +namespace System.Runtime.CompilerServices; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; + +/// +/// Used to indicate to the compiler that a method should be called +/// in its containing module's initializer. +/// +/// +/// When one or more valid methods +/// with this attribute are found in a compilation, the compiler will +/// emit a module initializer which calls each of the attributed methods. +/// +/// Certain requirements are imposed on any method targeted with this attribute: +/// - The method must be `static`. +/// - The method must be an ordinary member method, as opposed to a property accessor, constructor, local function, etc. +/// - The method must be parameterless. +/// - The method must return `void`. +/// - The method must not be generic or be contained in a generic type. +/// - The method's effective accessibility must be `internal` or `public`. +/// +/// The specification for module initializers in the .NET runtime can be found here: +/// https://github.com/dotnet/runtime/blob/master/docs/design/specs/Ecma-335-Augments.md#module-initializer +/// +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-7.0")] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: AttributeTargets.Method, + Inherited = false)] +#if PolyPublic +public +#endif +sealed class ModuleInitializerAttribute : + Attribute +{ +} + +#endif +``` +snippet source | anchor + + + +#### If the API is a missing instance method + +Add an extension method to `Polyfill_TYPE.cs` where `TYPE` is the type the method extending. So, for example, APIs that target `StreamWriter` go in `Polyfill_StreamWriter.cs`. + +Example: + + + +```cs +// + +#pragma warning disable + +#if MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0) + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; +using Link = System.ComponentModel.DescriptionAttribute; +using System.Threading; +using System.Threading.Tasks; + +static partial class Polyfill +{ + +#if TASKSEXTENSIONSREFERENCED + + /// + /// Asynchronously writes a character memory region to the stream. + /// + /// The character memory region to write to the stream. + /// + /// The token to monitor for cancellation requests. + /// The default value is . + /// + /// A task that represents the asynchronous write operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)")] + public static ValueTask WriteAsync( + this TextWriter target, + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) + { + // StreamReader doesn't accept cancellation token (pre-netstd2.1) + cancellationToken.ThrowIfCancellationRequested(); + + if (!MemoryMarshal.TryGetArray(buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.WriteAsync(segment.Array!, segment.Offset, segment.Count)); + } + + /// + /// Asynchronously writes the text representation of a character memory region to the stream, followed by a line terminator. + /// + /// The character memory region to write to the stream. + /// + /// The token to monitor for cancellation requests. + /// The default value is . + /// + /// A task that represents the asynchronous write operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync#system-io-textwriter-writelineasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)")] + public static ValueTask WriteLineAsync( + this TextWriter target, + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) + { + // StreamReader doesn't accept cancellation token (pre-netstd2.1) + cancellationToken.ThrowIfCancellationRequested(); + + if (!MemoryMarshal.TryGetArray(buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.WriteLineAsync(segment.Array!, segment.Offset, segment.Count)); + } + +#endif + + /// + /// Writes a character span to the text stream. + /// + /// The character span to write. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))")] + public static void Write( + this TextWriter target, + ReadOnlySpan buffer) + { + var pool = ArrayPool.Shared; + var array = pool.Rent(buffer.Length); + + try + { + buffer.CopyTo(new(array)); + target.Write(array, 0, buffer.Length); + } + finally + { + pool.Return(array); + } + } + + /// + /// Writes the text representation of a character span to the text stream, followed by a line terminator. + /// + /// The char span value to write to the text stream. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeline#system-io-textwriter-writeline(system-readonlyspan((system-char)))")] + public static void WriteLine( + this TextWriter target, + ReadOnlySpan buffer) + { + var pool = ArrayPool.Shared; + var array = pool.Rent(buffer.Length); + + try + { + buffer.CopyTo(new(array)); + target.WriteLine(array, 0, buffer.Length); + } + finally + { + pool.Return(array); + } + } +} +#endif +``` +snippet source | anchor + + + +### Add a test + +Add a for the new API to the Tests project. + +Extension method tests to `PolyfillTests_TYPE.cs` where `TYPE` is the type the method extending. So, for example, APIs that target `StreamWriter` go in `PolyfillTests_StreamWriter.cs`. For example: + + + +```cs +partial class PolyfillTests +{ + [Test] + public async Task StreamReaderReadAsync() + { + using var stream = new MemoryStream("value"u8.ToArray()); + var result = new char[5]; + var memory = new Memory(result); + using var reader = new StreamReader(stream); + var read = await reader.ReadAsync(memory); + Assert.AreEqual(5, read); + Assert.IsTrue("value".SequenceEqual(result)); + } + + [Test] + public async Task StreamReaderReadToEndAsync() + { + using var stream = new MemoryStream("value"u8.ToArray()); + using var reader = new StreamReader(stream); + var read = await reader.ReadToEndAsync(Cancel.None); + Assert.AreEqual("value", read); + } +} +``` +snippet source | anchor + + + +### Add documentation + +Add documentation for the API to the `readme.md`. + + +### Add to the Consume project + +Add a simple usage of the API to the Consume project. diff --git a/notes.md b/notes.md new file mode 100644 index 00000000..71109e0e --- /dev/null +++ b/notes.md @@ -0,0 +1 @@ +apidiff https://github.com/dotnet/core/blob/main/release-notes/8.0/8.0.1/api-diff/README.md diff --git a/readme.md b/readme.md index f51e30c5..de894c15 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![Build status](https://ci.appveyor.com/api/projects/status/s6eqqg4ipeovebgd?svg=true)](https://ci.appveyor.com/project/SimonCropp/Polyfill) [![Polyfill NuGet Status](https://img.shields.io/nuget/v/Polyfill.svg)](https://www.nuget.org/packages/Polyfill/) -Source only package that exposes newer .net and C# features to older runtimes. +Source only package that exposes newer .NET and C# features to older runtimes. The package targets `netstandard2.0` and is designed to support the following runtimes. @@ -12,6 +12,8 @@ The package targets `netstandard2.0` and is designed to support the following ru * `net5.0`, `net6.0`, `net7.0`, `net8.0` +**See [Milestones](../../milestones?state=closed) for release notes.** + ## Nuget https://nuget.org/packages/Polyfill/ @@ -19,12 +21,12 @@ https://nuget.org/packages/Polyfill/ ### SDK / LangVersion -This project uses features from the current stable SDK and c# language. As such consuming projects should target those: +This project uses features from the current stable SDK and C# language. As such consuming projects should target those: ### LangVersion -``` +```xml latest @@ -33,10 +35,10 @@ This project uses features from the current stable SDK and c# language. As such ### global.json -``` +```json { "sdk": { - "version": "7.0.202", + "version": "7.0.306", "rollForward": "latestFeature" } } @@ -52,7 +54,7 @@ The default type visibility for all polyfills is `internal`. This means it can b If Polyfill is being consumed in a solution that produce an app, then it is recommended to use the Polyfill nuget only in the root "app project" and enable `PolyPublic`. -``` +```xml true @@ -65,7 +67,7 @@ Then all consuming projects, like tests, will not need to use the Polyfill nuget If Polyfill is being consumed in a solution that produce a library (and usually a nuget), then the Polyfill nuget can be added to all projects. -If, however, `InternalsVisibileTo` is being used to expose APIs (for example to test projects), then the Polyfill nuget should be added only to the root library project. +If, however, `InternalsVisibleTo` is being used to expose APIs (for example to test projects), then the Polyfill nuget should be added only to the root library project. ## Included polyfills @@ -103,16 +105,10 @@ Reference: [init (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp ```cs class InitExample { - private int member; - - public int Member - { - get => member; - init => member = value; - } + public int Member { get; init; } } ``` -snippet source | anchor +snippet source | anchor @@ -169,7 +165,7 @@ public class Person * [SkipLocalsInitAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.skiplocalsinitattribute) -Reference: (SkipLocalsInit attribute)(https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/general#skiplocalsinit-attribute) +Reference: [SkipLocalsInitAttribute](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/general#skiplocalsinit-attribute) > the SkipLocalsInit attribute prevents the compiler from setting the .locals init flag when emitting to metadata. The SkipLocalsInit attribute is a single-use attribute and can be applied to a method, a property, a class, a struct, an interface, or a module, but not to an assembly. SkipLocalsInit is an alias for SkipLocalsInitAttribute. @@ -197,6 +193,8 @@ class SkipLocalsInitSample Reference: [Indices and ranges](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/ranges-indexes) +If consuming in a project that targets net461 or net462, a reference to System.ValueTuple is required. See [References: System.ValueTuple](#systemvaluetuple). + ```cs @@ -231,6 +229,8 @@ Reference: [Low Level Struct Improvements](https://github.com/dotnet/csharplang/ ```cs +#if !NET7_0_OR_GREATER + using System.Diagnostics.CodeAnalysis; struct UnscopedRefUsage @@ -239,8 +239,10 @@ struct UnscopedRefUsage [UnscopedRef] ref int Prop1 => ref field; } + +#endif ``` -snippet source | anchor +snippet source | anchor @@ -275,16 +277,19 @@ static class GuardUsage } } ``` -snippet source | anchor +snippet source | anchor ### InterpolatedStringHandler + * [AppendInterpolatedStringHandler](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendinterpolatedstringhandler) + * [DefaultInterpolatedStringHandler](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.defaultinterpolatedstringhandler) * [InterpolatedStringHandlerAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.interpolatedstringhandlerattribute) * [InterpolatedStringHandlerArgumentAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.interpolatedstringhandlerargumentattribute) + * [ISpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable) -Reference: [Write a custom string interpolation handler](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler) +References: [String Interpolation in C# 10 and .NET 6](https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/), [Write a custom string interpolation handler](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler) ### StringSyntaxAttribute @@ -333,7 +338,7 @@ Reference: [Improvements in native code interop in .NET 5.0](https://devblogs.mi ### SuppressGCTransition - * [SuppressGCTransitionAttribte](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.suppressgctransitionattribute) + * [SuppressGCTransitionAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.suppressgctransitionattribute) ### DisableRuntimeMarshalling @@ -343,51 +348,333 @@ Reference: [Improvements in native code interop in .NET 5.0](https://devblogs.mi ## Extensions -The class `PolyfillExtensions` includes the following extension methods: +The class `Polyfill` includes the following extension methods: + +> [!IMPORTANT] +> The methods using `AppendInterpolatedStringHandler` parameter are not extensions because the compiler prefers to use the overload with `string` parameter instead. + + +### Extension methods + +#### Boolean + + * `Boolean TryFormat(Span, Int32&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.boolean.tryformat) + + +#### Byte + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.byte.tryformat) + + +#### Dictionary + + * `Boolean Remove(TKey, TValue&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.remove) + + +#### IEnumerable + + * `IEnumerable Chunk(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk) + * `IEnumerable Except(TSource)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except?view=net-8.0#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0)))) + * `IEnumerable Except(TSource[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except?view=net-8.0#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0)))) + * `IEnumerable Except(TSource, IEqualityComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))) + * `IEnumerable Except(IEqualityComparer, TSource[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))) + * `TSource MaxBy(Func)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))) + * `TSource MaxBy(Func, IComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))) + * `TSource MinBy(Func)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))) + * `TSource MinBy(Func, IComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby?view=net-8.0#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))) + * `IEnumerable SkipLast(Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast) + * `HashSet ToHashSet(IEqualityComparer)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tohashset#system-linq-enumerable-tohashset-1(system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))) + + +#### IReadOnlyDictionary + + * `TValue GetValueOrDefault(TKey)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault) + * `TValue GetValueOrDefault(TKey, TValue)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault#system-collections-generic-collectionextensions-getvalueordefault-2(system-collections-generic-ireadonlydictionary((-0-1))-0-1)) + + +#### KeyValuePair + + * `Void Deconstruct(TKey&, TValue&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.keyvaluepair-2.deconstruct) + + +#### DateTime + + * `DateTime AddMicroseconds(Double)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.addmicroseconds) + * `Int32 Microsecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.microsecond) + * `Int32 Nanosecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.nanosecond) + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.tryformat) + + +#### DateTimeOffset + + * `DateTimeOffset AddMicroseconds(Double)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.addmicroseconds) + * `Int32 Microsecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.microsecond) + * `Int32 Nanosecond()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.nanosecond) + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tryformat) + + +#### Decimal + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.decimal.tryformat) + + +#### Process + + * `Task WaitForExitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync) + + +#### Double + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.double.tryformat) + + +#### Guid + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.guid.tryformat#system-guid-tryformat(system-span((system-char))-system-int32@-system-readonlyspan((system-char)))) + + +#### Int16 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int16.tryformat) + + +#### Int32 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int32.tryformat) + + +#### Int64 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int64.tryformat) + + +#### Stream + + * `Task CopyToAsync(Stream, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.copytoasync#system-io-stream-copytoasync(system-io-stream-system-threading-cancellationtoken)) + * `ValueTask ReadAsync(Memory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.readasync#system-io-stream-readasync(system-memory((system-byte))-system-threading-cancellationtoken)) + * `ValueTask WriteAsync(ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.writeasync#system-io-stream-writeasync(system-readonlymemory((system-byte))-system-threading-cancellationtoken)) + + +#### TextReader + + * `ValueTask ReadAsync(Memory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readasync#system-io-textreader-readasync(system-memory((system-char))-system-threading-cancellationtoken)) + * `Task ReadLineAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readlineasync(system-threading-cancellationtoken)) + * `Task ReadToEndAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readtoendasync(system-threading-cancellationtoken)) + + +#### TextWriter + + * `Void Write(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))) + * `ValueTask WriteAsync(ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)) + * `Void WriteLine(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeline#system-io-textwriter-writeline(system-readonlyspan((system-char)))) + * `ValueTask WriteLineAsync(ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync#system-io-textwriter-writelineasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)) + + +#### HttpClient + + * `Task GetByteArrayAsync(String, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync#system-net-http-httpclient-getbytearrayasync(system-string-system-threading-cancellationtoken)) + * `Task GetByteArrayAsync(Uri, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync#system-net-http-httpclient-getbytearrayasync(system-uri-system-threading-cancellationtoken)) + * `Task GetStreamAsync(String, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstreamasync#system-net-http-httpclient-getstreamasync(system-string-system-threading-cancellationtoken)) + * `Task GetStreamAsync(Uri, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstreamasync#system-net-http-httpclient-getstreamasync(system-uri-system-threading-cancellationtoken)) + * `Task GetStringAsync(String, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstringasync#system-net-http-httpclient-getstringasync(system-string-system-threading-cancellationtoken)) + * `Task GetStringAsync(Uri, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstringasync#system-net-http-httpclient-getstringasync(system-uri-system-threading-cancellationtoken)) + + +#### HttpContent + + * `Task ReadAsByteArrayAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasbytearrayasync#system-net-http-httpcontent-readasbytearrayasync(system-threading-cancellationtoken)) + * `Task ReadAsStreamAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasstreamasync#system-net-http-httpcontent-readasstreamasync(system-threading-cancellationtoken)) + * `Task ReadAsStringAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasstringasync#system-net-http-httpcontent-readasstringasync(system-threading-cancellationtoken)) + + +#### ReadOnlySpan + + * `Boolean EndsWith(String, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.endswith#system-memoryextensions-endswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + * `Boolean SequenceEqual(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + * `Boolean StartsWith(String, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith#system-memoryextensions-startswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + + +#### ReadOnlySpan + + * `Boolean Contains(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.contains#system-memoryextensions-contains-1(system-readonlyspan((-0))-0)) + + +#### Reflection.EventInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` + + +#### Reflection.FieldInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` + + +#### Reflection.MemberInfo + + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean HasSameMetadataDefinitionAs(Reflection.MemberInfo)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.hassamemetadatadefinitionas) + * `Boolean IsNullable()` -### String +#### Reflection.ParameterInfo - * `bool StartsWith(this string, char)` - * `bool EndsWith(this string, char)` + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` -### Memory +#### Reflection.PropertyInfo -[System.Memory](#systemmemory) reference required. + * `Reflection.NullabilityState GetNullability()` + * `Reflection.NullabilityInfo GetNullabilityInfo()` + * `Boolean IsNullable()` - * `bool Contains(this ReadOnlySpan, char)` - * `void Append(this StringBuilder, ReadOnlySpan)` - * `bool SequenceEqual(this ReadOnlySpan, string)` - * `bool Equals(this StringBuilder, ReadOnlySpan)` - * `bool SequenceEqual(this Span, string)` +#### SByte -### Stream / StreamReader + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.sbyte.tryformat) -[System.Threading.Tasks.Extensions](#systemthreadingtasksextensions) reference required. - * `ValueTask ReadAsync(this Stream, Memory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.readasync#system-io-stream-readasync(system-memory((system-byte))-system-threading-cancellationtoken)) - * `ValueTask WriteAsync(this Stream, ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.writeasync#system-io-stream-writeasync(system-readonlymemory((system-byte))-system-threading-cancellationtoken)) - * `ValueTask ReadAsync(this StreamReader, Memory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader.readasync#system-io-streamreader-readasync(system-memory((system-char))-system-threading-cancellationtoken)) - * `ValueTask WriteAsync(this StreamWriter, ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.streamwriter.writeasync#system-io-streamwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)) +#### Single + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.single.tryformat) -### KeyValuePair - * `public static void Deconstruct(this KeyValuePair, out TKey, out TValue)` +#### Span + + * `Boolean EndsWith(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.endswith#system-memoryextensions-endswith-1(system-span((-0))-system-readonlyspan((-0)))) + * `Boolean SequenceEqual(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-span((-0))-system-readonlyspan((-0)))) + * `Boolean StartsWith(String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith#system-memoryextensions-startswith-1(system-span((-0))-system-readonlyspan((-0)))) + + +#### Span + + * `Boolean Contains(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.contains#system-memoryextensions-contains-1(system-span((-0))-0)) + + +#### String + + * `Boolean Contains(String, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-string-system-stringcomparison)) + * `Boolean Contains(Char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)) + * `Void CopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.copyto) + * `Boolean EndsWith(Char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)) + * `Int32 GetHashCode(StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.gethashcode#system-string-gethashcode(system-stringcomparison)) + * `String[] Split(Char, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.split#system-string-split(system-char-system-stringsplitoptions)) + * `String[] Split(Char, Int32, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.split#system-string-split(system-char-system-int32-system-stringsplitoptions)) + * `Boolean StartsWith(Char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)) + * `Boolean TryCopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.trycopyto) + + +#### Regex + + * `ValueMatchEnumerator EnumerateMatches(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char)))) + * `ValueMatchEnumerator EnumerateMatches(ReadOnlySpan, Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char))-system-int32)) + * `Boolean IsMatch(ReadOnlySpan, Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-int32)) + * `Boolean IsMatch(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char)))) + + +#### StringBuilder + + * `StringBuilder Append(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-readonlyspan((system-char)))) + * `StringBuilder Append(StringBuilder, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `StringBuilder Append(StringBuilder, IFormatProvider, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `StringBuilder AppendJoin(String, String[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-string-system-string())) + * `StringBuilder AppendJoin(String, Object[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-string-system-object())) + * `StringBuilder AppendJoin(Char, String[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-char-system-string())) + * `StringBuilder AppendJoin(Char, Object[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-char-system-object())) + * `StringBuilder AppendJoin(Char, T[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin-1(system-char-system-collections-generic-ienumerable((-0)))) + * `StringBuilder AppendJoin(String, T[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin-1(system-string-system-collections-generic-ienumerable((-0)))) + * `StringBuilder AppendLine(StringBuilder, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `StringBuilder AppendLine(StringBuilder, IFormatProvider, AppendInterpolatedStringHandler&)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)) + * `Void CopyTo(Int32, Span, Int32)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.copyto#system-text-stringbuilder-copyto(system-int32-system-span((system-char))-system-int32)) + * `Boolean Equals(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.equals#system-text-stringbuilder-equals(system-readonlyspan((system-char)))) + + +#### CancellationToken + + * `CancellationTokenRegistration Register(Action, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.register#system-threading-cancellationtoken-register(system-action((system-object-system-threading-cancellationtoken))-system-object)) + * `CancellationTokenRegistration UnsafeRegister(Action, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object))-system-object)) + * `CancellationTokenRegistration UnsafeRegister(Action, Object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object-system-threading-cancellationtoken))-system-object)) + + +#### CancellationTokenSource + + * `Task CancelAsync()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync) + + +#### Task + + * `Task WaitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-threading-cancellationtoken)) + * `Task WaitAsync(TimeSpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-timespan)) + * `Task WaitAsync(TimeSpan, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-timespan-system-threading-cancellationtoken)) + + +#### Task + + * `Task WaitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-threading-cancellationtoken)) + * `Task WaitAsync(TimeSpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.waitasync#system-threading-tasks-task-1-waitasync(system-timespan)) + * `Task WaitAsync(TimeSpan, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.waitasync#system-threading-tasks-task-1-waitasync(system-timespan-system-threading-cancellationtoken)) + + +#### TimeSpan + + * `Int32 Microseconds()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.microseconds) + * `Int32 Nanoseconds()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.nanoseconds) + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.tryformat#system-timespan-tryformat(system-span((system-byte))-system-int32@-system-readonlyspan((system-char))-system-iformatprovider)) + + +#### Type + + * `Boolean IsGenericMethodParameter()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.type.isgenericmethodparameter) + + +#### UInt16 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uint16.tryformat) + + +#### UInt32 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uint32.tryformat) + + +#### UInt64 + + * `Boolean TryFormat(Span, Int32&, ReadOnlySpan, IFormatProvider)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.uint64.tryformat) + + +### Static helpers + +#### EnumPolyfill + + * `String[] GetNames()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.enum.getnames) + * `TEnum[] GetValues()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.enum.getvalues) + + +#### RegexPolyfill + + * `Boolean IsMatch(ReadOnlySpan, String, RegexOptions, TimeSpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions-system-timespan)) + * `Boolean IsMatch(ReadOnlySpan, String, RegexOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions)) + * `Boolean IsMatch(ReadOnlySpan, String)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string)) + + ## References -If any of the below reference are included, the related polyfills will be disabled. +If any of the below reference are not included, the related polyfills will be disabled. ### System.ValueTuple If consuming in a project that targets `net461` or `net462`, a reference to [System.ValueTuple](https://www.nuget.org/packages/System.ValueTuple/) nuget is required. -``` +```xml @@ -396,9 +683,9 @@ If consuming in a project that targets `net461` or `net462`, a reference to [Sys ### System.Memory -If consuming in a project that targets `netstandard`, `netframework`, or `netcoreapp2*`, a reference to [System.Memory](https://www.nuget.org/packages/System.Memory/) nuget is required. +If using Span APIs and consuming in a project that targets `netstandard`, `netframework`, or `netcoreapp2*`, a reference to [System.Memory](https://www.nuget.org/packages/System.Memory/) nuget is required. -``` +```xml snippet source | anchor + + + +### NullabilityInfoExtensions + +`NullabilityInfoExtensions` provides static and thread safe wrapper around `NullabilityInfoContext`. It adds three extension methods to each of ParameterInfo, PropertyInfo, EventInfo, and FieldInfo. + + * `GetNullabilityInfo`: returns the `NullabilityInfo` for the target info. + * `GetNullability`: returns the `NullabilityState` for the state (`NullabilityInfo.ReadState` or `NullabilityInfo.WriteState` depending on which has more info) of target info. + * `IsNullable`: given the state (`NullabilityInfo.ReadState` or `NullabilityInfo.WriteState` depending on which has more info) of the info: + * Returns true if state is `NullabilityState.Nullable`. + * Returns false if state is `NullabilityState.NotNull`. + * Throws an exception if state is `NullabilityState.Unknown`. + + ## Alternatives +### PolyShim + +https://github.com/Tyrrrz/PolyShim + + ### PolySharp https://github.com/Sergio0694/PolySharp +### Theraot.Core + +https://github.com/theraot/Theraot + + ### Combination of * https://github.com/manuelroemer/Nullable diff --git a/src/.editorconfig b/src/.editorconfig index 7542ee3a..99a756c1 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -13,21 +13,18 @@ charset = utf-8 # Microsoft .NET properties +trim_trailing_whitespace = true csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion -csharp_style_namespace_declarations = file_scoped:error -dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined +resharper_namespace_body = file_scoped dotnet_naming_rule.private_constants_rule.severity = warning dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols -dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_instance_fields_rule.severity = warning dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols -dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_static_fields_rule.severity = warning dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols -dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_static_readonly_rule.severity = warning dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols @@ -49,8 +46,6 @@ dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none # ReSharper properties -resharper_apply_on_completion = true -resharper_csharp_wrap_lines = false resharper_object_creation_when_type_not_evident = target_typed # ReSharper inspection severities @@ -79,9 +74,9 @@ resharper_redundant_suppress_nullable_warning_expression_highlighting = error resharper_redundant_using_directive_highlighting = error resharper_redundant_verbatim_string_prefix_highlighting = error resharper_replace_substring_with_range_indexer_highlighting = warning -resharper_suggest_var_or_type_built_in_types_highlighting = hint -resharper_suggest_var_or_type_elsewhere_highlighting = hint -resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_suggest_var_or_type_built_in_types_highlighting = error +resharper_suggest_var_or_type_elsewhere_highlighting = error +resharper_suggest_var_or_type_simple_types_highlighting = error resharper_unnecessary_whitespace_highlighting = error resharper_use_await_using_highlighting = warning resharper_use_deconstruction_highlighting = warning @@ -113,9 +108,10 @@ csharp_style_var_elsewhere = true:error # Prefer method-like constructs to have a block body csharp_style_expression_bodied_methods = true:error +csharp_style_expression_bodied_local_functions = true:error csharp_style_expression_bodied_constructors = true:error csharp_style_expression_bodied_operators = true:error -csharp_place_expr_method_on_single_line = false +resharper_place_expr_method_on_single_line = false # Prefer property-like constructs to have an expression-body csharp_style_expression_bodied_properties = true:error @@ -128,42 +124,49 @@ csharp_style_pattern_matching_over_as_with_null_check = true:error csharp_style_inlined_variable_declaration = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion -csharp_style_prefer_switch_expression = true:suggestion # Newline settings #csharp_new_line_before_open_brace = all:error -max_array_initializer_elements_on_line = 1 +resharper_max_array_initializer_elements_on_line = 1 csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true -csharp_place_type_constraints_on_same_line = false:error -csharp_wrap_before_first_type_parameter_constraint = true:error -csharp_wrap_extends_list_style = true:error -csharp_wrap_after_dot_in_method_calls false -csharp_wrap_before_binary_pattern_op false -resharper_csharp_wrap_object_and_collection_initializer_style = chop_always -resharper_csharp_place_simple_initializer_on_single_line = false +resharper_wrap_before_first_type_parameter_constraint = true +resharper_wrap_extends_list_style = chop_always +resharper_wrap_after_dot_in_method_calls = false +resharper_wrap_before_binary_pattern_op = false +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_place_simple_initializer_on_single_line = false dotnet_style_require_accessibility_modifiers = never:error resharper_place_type_constraints_on_same_line = false +resharper_blank_lines_inside_namespace = 0 +resharper_blank_lines_after_file_scoped_namespace_directive = 1 +resharper_blank_lines_inside_type = 0 + +insert_final_newline = false +resharper_place_attribute_on_same_line = false +resharper_space_around_lambda_arrow = true +resharper_place_constructor_initializer_on_same_line = false #braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces -braces_for_ifelse = required -braces_for_foreach = required -braces_for_while = required -braces_for_dowhile = required -braces_for_lock = required -braces_for_fixed = required -braces_for_for = required +resharper_braces_for_ifelse = required +resharper_braces_for_foreach = required +resharper_braces_for_while = required +resharper_braces_for_dowhile = required +resharper_braces_for_lock = required +resharper_braces_for_fixed = required +resharper_braces_for_for = required # Xml files -[*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props}] +[*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props,fsproj}] indent_size = 2 # https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi -blank_line_after_pi = false -space_before_self_closing = true +resharper_blank_line_after_pi = false +resharper_space_before_self_closing = true +ij_xml_space_inside_empty_tag = true [*.json] indent_size = 2 \ No newline at end of file diff --git a/src/.gitattributes b/src/.gitattributes index 44a18354..4f905c91 100644 --- a/src/.gitattributes +++ b/src/.gitattributes @@ -2,6 +2,9 @@ *.snk binary *.png binary -*.verified.txt text eol=lf -*.verified.xml text eol=lf -*.verified.json text eol=lf \ No newline at end of file +*.verified.txt text eol=lf working-tree-encoding=UTF-8 +*.verified.xml text eol=lf working-tree-encoding=UTF-8 +*.verified.json text eol=lf working-tree-encoding=UTF-8 + +.editorconfig text eol=lf working-tree-encoding=UTF-8 +Shared.sln.DotSettings text eol=lf working-tree-encoding=UTF-8 \ No newline at end of file diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index c6bad6a7..9eb2745d 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -1,13 +1,22 @@ +// ReSharper disable RedundantUsingDirective +#nullable enable + using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; +#pragma warning disable CS4014 class Consume { @@ -30,10 +39,9 @@ class Consume type = typeof(RequiredMemberAttribute); type = typeof(SetsRequiredMembersAttribute); type = typeof(SkipLocalsInitAttribute); - type = typeof(TupleElementNamesAttribute); + //TODO: + // type = typeof(TupleElementNamesAttribute); type = typeof(DebuggerNonUserCodeAttribute); - type = typeof(ValueTuple<>); - type = typeof(ValueTuple); type = typeof(UnscopedRefAttribute); type = typeof(InterpolatedStringHandlerArgumentAttribute); type = typeof(InterpolatedStringHandlerAttribute); @@ -44,7 +52,8 @@ class Consume type = typeof(RequiresUnreferencedCodeAttribute); type = typeof(UnconditionalSuppressMessageAttribute); type = typeof(CompilerFeatureRequiredAttribute); - type = typeof(AsyncMethodBuilderAttribute); + //TODO: + //type = typeof(AsyncMethodBuilderAttribute); type = typeof(ObsoletedOSPlatformAttribute); type = typeof(SupportedOSPlatformAttribute); type = typeof(SupportedOSPlatformGuardAttribute); @@ -56,13 +65,64 @@ class Consume type = typeof(SuppressGCTransitionAttribute); type = typeof(DisableRuntimeMarshallingAttribute); type = typeof(RequiresUnreferencedCodeAttribute); + type = typeof(UnreachableException); +#if (NET46X && VALUETUPLEREFERENCED) || NET47X || NET48X || NETSTANDARD2_0 || NETCOREAPP2X var range = "value"[1..]; var index = "value"[^2]; +#endif + var startsWith = "value".StartsWith('a'); var endsWith = "value".EndsWith('a'); + + var enumerable = (IEnumerable) new List + { + "a", + "b" + }; + var append = enumerable.Append("c"); + var maxBy = enumerable.MaxBy(_ => _); + var chunk = enumerable.Chunk(3); + var minBy = enumerable.MinBy(_ => _); + var skipLast = enumerable.SkipLast(1); + + var dictionary = new Dictionary + { + { + "key", "value" + } + }; + + dictionary.GetValueOrDefault("key"); + dictionary.GetValueOrDefault("key", "default"); + + var split = "a b".Split(' ', StringSplitOptions.RemoveEmptyEntries); + split = "a b".Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + var contains = "a b".Contains(' '); + + // Test to make sure there are no clashes in the Polyfill code with classes that + // might be defined in user code. See comments in Debug.cs for more details. + Debug.Log("Test log to make sure this is working"); + } + +#if HTTPREFERENCED + + static void Http() + { + new HttpClient().GetStreamAsync("", CancellationToken.None); + new HttpClient().GetStreamAsync(new Uri(""), CancellationToken.None); + new HttpClient().GetByteArrayAsync("", CancellationToken.None); + new HttpClient().GetByteArrayAsync(new Uri(""), CancellationToken.None); + new HttpClient().GetStringAsync("", CancellationToken.None); + new HttpClient().GetStringAsync(new Uri(""), CancellationToken.None); + + new ByteArrayContent(Array.Empty()).ReadAsStreamAsync(CancellationToken.None); + new ByteArrayContent(Array.Empty()).ReadAsByteArrayAsync(CancellationToken.None); + new ByteArrayContent(Array.Empty()).ReadAsStringAsync(CancellationToken.None); } +#endif + void KeyValuePairDeconstruct(IEnumerable> variables) { foreach (var (name, value) in variables) @@ -70,18 +130,82 @@ void KeyValuePairDeconstruct(IEnumerable> variables } } +#if (NET46X && VALUETUPLEREFERENCED) || NET47X || NET48X || NETSTANDARD2_0 || NETCOREAPP2X + static (string value1, bool value2) NamedTupleMethod() => new("value", false); +#endif + + #pragma warning disable ExperimentalMethod + static void ExperimentalMethodUsage() => + ExperimentalMethod(); + + [Experimental("ExperimentalMethod")] + static void ExperimentalMethod() + { + } + + async Task CancellationTokenSource() + { + var source = new CancellationTokenSource(); + await source.CancelAsync(); + } + + void CancellationTokenUnsafeRegister() + { + var source = new CancellationTokenSource(); + var token = source.Token; + token.UnsafeRegister(state => {}, null); + token.UnsafeRegister((state, token) => {}, null); + } + + async Task ProcessWaitForExitAsync() + { + var process = new Process(); + await process.WaitForExitAsync(); + } + + async Task StreamReaderReadToEndAsync() + { + var reader = new StreamReader(new MemoryStream()); + var read = await reader.ReadToEndAsync(CancellationToken.None); + } + + async Task StreamReaderReadLineAsync() + { + TextReader reader = new StreamReader(new MemoryStream()); + var read = await reader.ReadLineAsync(CancellationToken.None); + } + + void WaitAsync() + { + var action = () => {}; + var func = () => 0; + new Task(action).WaitAsync(CancellationToken.None); + new Task(action).WaitAsync(TimeSpan.Zero); + new Task(action).WaitAsync(TimeSpan.Zero, CancellationToken.None); + new Task(func).WaitAsync(CancellationToken.None); + new Task(func).WaitAsync(TimeSpan.Zero); + new Task(func).WaitAsync(TimeSpan.Zero, CancellationToken.None); + } + +#if MEMORYREFERENCED + async Task StreamReaderReadAsync() { - using var stream = new MemoryStream("value"u8.ToArray()); var result = new char[5]; var memory = new Memory(result); - using var reader = new StreamReader(stream); + var reader = new StreamReader(new MemoryStream()); var read = await reader.ReadAsync(memory); } + void RegexIsMatch() + { + var regex = new Regex("result"); + regex.IsMatch("value".AsSpan()); + } + async Task StreamReadAsync() { var input = new byte[] @@ -95,14 +219,29 @@ async Task StreamReadAsync() var read = await stream.ReadAsync(memory); } - void SpanContains() + void StringBuilderCopyTo() { - var contains = "value".AsSpan().Contains('e'); + var builder = new StringBuilder("value"); + var span = new Span(new char[1]); + builder.CopyTo(0, span, 1); } void SpanSequenceEqual() { - var sequenceEqual = "value".AsSpan().SequenceEqual("value"); + var result = "value".AsSpan().SequenceEqual("value"); + } + + void SpanStartsWith() + { + var startsWith = "value".AsSpan().StartsWith("value"); + startsWith = "value".AsSpan().StartsWith("value", StringComparison.Ordinal); + } + + + void SpanEndsWith() + { + var result = "value".AsSpan().EndsWith("value"); + result = "value".AsSpan().EndsWith("value", StringComparison.Ordinal); } void SpanStringBuilderAppend() @@ -116,4 +255,21 @@ void StringEqualsSpan() var builder = new StringBuilder("value"); var equals = builder.Equals("value".AsSpan()); } + +#endif + + void IsGenericMethodParameter() + { + var result = typeof(string).IsGenericMethodParameter(); + } + + void HasSameMetadataDefinitionAs(MemberInfo info) + { + var result = info.HasSameMetadataDefinitionAs(info); + } + + void GetMemberWithSameMetadataDefinitionAs(MemberInfo info) + { + var result = typeof(string).GetMemberWithSameMetadataDefinitionAs(info); + } } \ No newline at end of file diff --git a/src/Consume/Consume.csproj b/src/Consume/Consume.csproj index d8f98c6f..cf5c25bf 100644 --- a/src/Consume/Consume.csproj +++ b/src/Consume/Consume.csproj @@ -5,26 +5,11 @@ $(TargetFrameworks);netstandard2.0;netstandard2.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 - - + + - - Pollyfill\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Nullable\%(RecursiveDir)%(Filename).cs - - - Pollyfill\IndexRange\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Trimming\%(RecursiveDir)%(Filename).cs - - - Pollyfill\PlatformCompatibility\%(RecursiveDir)%(Filename).cs - - - + + \ No newline at end of file diff --git a/src/Consume/Debug.cs b/src/Consume/Debug.cs new file mode 100644 index 00000000..0b05cf42 --- /dev/null +++ b/src/Consume/Debug.cs @@ -0,0 +1,29 @@ +using System; + +// Test to make sure there are no clashes in the Polyfill code with classes that might be defined in user code. +// +// Some codebases, for better or for worse, define classes with names that match common classes in the base +// .NET libraries, like "Debug". This works find in those codebases because they are usually contained in the +// same namespace, or are guarded in some other way. But if a Polyfill attribute is imported and uses code like: +// Debug.Assert(genericParameter.IsGenericParameter); +// instead of a fully qualified name like: +// System.Debug.Assert(genericParameter.IsGenericParameter); +// then users of Polyfill will get errors like the following, which they can't easily fix: +// 'Debug' does not contain a definition for 'Assert' +// +// So, this file defines a custom Debug class to make sure that Polyfill code doesn't clash with user code. + +class Debug +{ + public static void Log(string content) => Console.WriteLine(content); + + public static void Log(ConsoleColor color, string content) + { + Console.ForegroundColor = color; + Console.WriteLine(content); + Console.ResetColor(); + } + + public static void LogWarning(string content) => Log(ConsoleColor.Yellow, content); + public static void LogError(string content) => Log(ConsoleColor.Red, content); +} diff --git a/src/Consume/UnscopedRefUsage.cs b/src/Consume/UnscopedRefUsage.cs index f6552db5..dce73822 100644 --- a/src/Consume/UnscopedRefUsage.cs +++ b/src/Consume/UnscopedRefUsage.cs @@ -1,8 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +#if !NET7_0_OR_GREATER + +using System.Diagnostics.CodeAnalysis; struct UnscopedRefUsage { int field; [UnscopedRef] ref int Prop1 => ref field; -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/src/ConsumeClassicReferences/ConsumeClassicReferences.csproj b/src/ConsumeClassicReferences/ConsumeClassicReferences.csproj new file mode 100644 index 00000000..0d6d46da --- /dev/null +++ b/src/ConsumeClassicReferences/ConsumeClassicReferences.csproj @@ -0,0 +1,16 @@ + + + true + net461;net462;net47;net471;net472;net48;net481;net6.0-windows + $(TargetFrameworks);netstandard2.0;netstandard2.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/ConsumeIndirect/ConsumeIndirect.csproj b/src/ConsumeIndirect/ConsumeIndirect.csproj new file mode 100644 index 00000000..e121f76f --- /dev/null +++ b/src/ConsumeIndirect/ConsumeIndirect.csproj @@ -0,0 +1,16 @@ + + + true + net461;net462;net47;net471;net472;net48;net481;net6.0-windows + $(TargetFrameworks);netstandard2.0;netstandard2.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/ConsumeNoRefs/ConsumeNoRefs.csproj b/src/ConsumeNoRefs/ConsumeNoRefs.csproj new file mode 100644 index 00000000..b3ff55de --- /dev/null +++ b/src/ConsumeNoRefs/ConsumeNoRefs.csproj @@ -0,0 +1,12 @@ + + + true + net461;net462;net47;net471;net472;net48;net481;net6.0-windows + $(TargetFrameworks);netstandard2.0;netstandard2.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 + + + + + + + \ No newline at end of file diff --git a/src/ConsumeTasksWithNoMemory/ConsumeTasksWithNoMemory.csproj b/src/ConsumeTasksWithNoMemory/ConsumeTasksWithNoMemory.csproj new file mode 100644 index 00000000..60ebae2e --- /dev/null +++ b/src/ConsumeTasksWithNoMemory/ConsumeTasksWithNoMemory.csproj @@ -0,0 +1,12 @@ + + + true + netstandard2.0 + + + + + + + + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9635e01b..d12a618a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,14 +1,17 @@ - 1.4.0 + 2.5.0 1.0.0 Polyfill true false - CS1591;NETSDK1138 - 11 + CS1591;NETSDK1138;NU1901;NU1902;NU1903 + false + 12 false true + true + true \ No newline at end of file diff --git a/src/NoRefsTests/NoRefsTests.csproj b/src/NoRefsTests/NoRefsTests.csproj index 698009c4..f99e76b0 100644 --- a/src/NoRefsTests/NoRefsTests.csproj +++ b/src/NoRefsTests/NoRefsTests.csproj @@ -1,39 +1,22 @@ - + net462;net472;net48;net6.0-windows $(TargetFrameworks);netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 - - - Pollyfill\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Nullable\%(RecursiveDir)%(Filename).cs - - - Pollyfill\IndexRange\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Trimming\%(RecursiveDir)%(Filename).cs - - - Pollyfill\PlatformCompatibility\%(RecursiveDir)%(Filename).cs - + + - - - - - + + + + + + - - + + \ No newline at end of file diff --git a/src/Polyfill.sln b/src/Polyfill.sln index c5fb7d40..82f829b8 100644 --- a/src/Polyfill.sln +++ b/src/Polyfill.sln @@ -18,14 +18,25 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polyfill", "Polyfill\Polyfill.csproj", "{698FB675-3480-4107-8CAE-51452C6138CE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{CA1869D1-4531-40C7-AE55-5885F4DD8448}" + ProjectSection(ProjectDependencies) = postProject + {32C38E3C-4040-455F-A27D-4EA5DB0F8EFA} = {32C38E3C-4040-455F-A27D-4EA5DB0F8EFA} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Consume", "Consume\Consume.csproj", "{32C38E3C-4040-455F-A27D-4EA5DB0F8EFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnsafeTests", "UnsafeTests\UnsafeTests.csproj", "{F49A3C33-48A3-4954-9AC0-5C7B30AC2B2B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicTests", "PublicTests\PublicTests.csproj", "{9FBD54A6-461C-4754-B77A-222E06BF89B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumeClassicReferences", "ConsumeClassicReferences\ConsumeClassicReferences.csproj", "{CF7D4778-6A32-4E7D-B80B-3507974B443B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consume", "Consume\Consume.csproj", "{32C38E3C-4040-455F-A27D-4EA5DB0F8EFA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumeIndirect", "ConsumeIndirect\ConsumeIndirect.csproj", "{955038AF-1073-4BB0-8AF7-D4597B7C2DAB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsafeTests", "UnsafeTests\UnsafeTests.csproj", "{F49A3C33-48A3-4954-9AC0-5C7B30AC2B2B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoRefsTests", "NoRefsTests\NoRefsTests.csproj", "{A9EEAECD-A8B2-45C1-9A9D-94443601CB5F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoRefsTests", "NoRefsTests\NoRefsTests.csproj", "{B46221EE-4806-423A-B21A-36E6B1D50027}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumeNoRefs", "ConsumeNoRefs\ConsumeNoRefs.csproj", "{B4DC96CA-C700-499F-A9A2-0C767DCF8C30}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicTests", "PublicTests\PublicTests.csproj", "{9FBD54A6-461C-4754-B77A-222E06BF89B6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumeTasksWithNoMemory", "ConsumeTasksWithNoMemory\ConsumeTasksWithNoMemory.csproj", "{96EF1E04-5862-4D9E-B800-A6402F1ADF7A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -37,10 +48,6 @@ Global {698FB675-3480-4107-8CAE-51452C6138CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {698FB675-3480-4107-8CAE-51452C6138CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {698FB675-3480-4107-8CAE-51452C6138CE}.Release|Any CPU.Build.0 = Release|Any CPU - {B4BAD33E-CF70-448F-AAF1-ED8941E5C875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4BAD33E-CF70-448F-AAF1-ED8941E5C875}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4BAD33E-CF70-448F-AAF1-ED8941E5C875}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4BAD33E-CF70-448F-AAF1-ED8941E5C875}.Release|Any CPU.Build.0 = Release|Any CPU {CA1869D1-4531-40C7-AE55-5885F4DD8448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA1869D1-4531-40C7-AE55-5885F4DD8448}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA1869D1-4531-40C7-AE55-5885F4DD8448}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -53,14 +60,30 @@ Global {F49A3C33-48A3-4954-9AC0-5C7B30AC2B2B}.Debug|Any CPU.Build.0 = Debug|Any CPU {F49A3C33-48A3-4954-9AC0-5C7B30AC2B2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {F49A3C33-48A3-4954-9AC0-5C7B30AC2B2B}.Release|Any CPU.Build.0 = Release|Any CPU - {B46221EE-4806-423A-B21A-36E6B1D50027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B46221EE-4806-423A-B21A-36E6B1D50027}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B46221EE-4806-423A-B21A-36E6B1D50027}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B46221EE-4806-423A-B21A-36E6B1D50027}.Release|Any CPU.Build.0 = Release|Any CPU {9FBD54A6-461C-4754-B77A-222E06BF89B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9FBD54A6-461C-4754-B77A-222E06BF89B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FBD54A6-461C-4754-B77A-222E06BF89B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FBD54A6-461C-4754-B77A-222E06BF89B6}.Release|Any CPU.Build.0 = Release|Any CPU + {CF7D4778-6A32-4E7D-B80B-3507974B443B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF7D4778-6A32-4E7D-B80B-3507974B443B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF7D4778-6A32-4E7D-B80B-3507974B443B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF7D4778-6A32-4E7D-B80B-3507974B443B}.Release|Any CPU.Build.0 = Release|Any CPU + {955038AF-1073-4BB0-8AF7-D4597B7C2DAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {955038AF-1073-4BB0-8AF7-D4597B7C2DAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {955038AF-1073-4BB0-8AF7-D4597B7C2DAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {955038AF-1073-4BB0-8AF7-D4597B7C2DAB}.Release|Any CPU.Build.0 = Release|Any CPU + {A9EEAECD-A8B2-45C1-9A9D-94443601CB5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9EEAECD-A8B2-45C1-9A9D-94443601CB5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9EEAECD-A8B2-45C1-9A9D-94443601CB5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9EEAECD-A8B2-45C1-9A9D-94443601CB5F}.Release|Any CPU.Build.0 = Release|Any CPU + {B4DC96CA-C700-499F-A9A2-0C767DCF8C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4DC96CA-C700-499F-A9A2-0C767DCF8C30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4DC96CA-C700-499F-A9A2-0C767DCF8C30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4DC96CA-C700-499F-A9A2-0C767DCF8C30}.Release|Any CPU.Build.0 = Release|Any CPU + {96EF1E04-5862-4D9E-B800-A6402F1ADF7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96EF1E04-5862-4D9E-B800-A6402F1ADF7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96EF1E04-5862-4D9E-B800-A6402F1ADF7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96EF1E04-5862-4D9E-B800-A6402F1ADF7A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Polyfill/CallerArgumentExpressionAttribute.cs b/src/Polyfill/CallerArgumentExpressionAttribute.cs index bd326d54..34eaf643 100644 --- a/src/Polyfill/CallerArgumentExpressionAttribute.cs +++ b/src/Polyfill/CallerArgumentExpressionAttribute.cs @@ -1,27 +1,38 @@ -#if NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X +// -#pragma warning disable +#if NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; -namespace System.Runtime.CompilerServices; - +/// +/// Indicates that a parameter captures the expression passed for another parameter as a string. +/// [ExcludeFromCodeCoverage] [DebuggerNonUserCode] [AttributeUsage(AttributeTargets.Parameter)] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerargumentexpressionattribute")] #if PolyPublic public #endif sealed class CallerArgumentExpressionAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the parameter whose expression should be captured as a string. + /// public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + /// + /// Gets the name of the parameter whose expression should be captured as a string. + /// public string ParameterName { get; } } diff --git a/src/Polyfill/CompilerFeatureRequiredAttribute.cs b/src/Polyfill/CompilerFeatureRequiredAttribute.cs index 7ee92224..28fb64c7 100644 --- a/src/Polyfill/CompilerFeatureRequiredAttribute.cs +++ b/src/Polyfill/CompilerFeatureRequiredAttribute.cs @@ -1,14 +1,12 @@ -#if !NET7_0_OR_GREATER +// -#pragma warning disable +#if !NET7_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; - -namespace System.Runtime.CompilerServices; +using Link = System.ComponentModel.DescriptionAttribute; /// /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. @@ -19,6 +17,7 @@ namespace System.Runtime.CompilerServices; validOn: AttributeTargets.All, AllowMultiple = true, Inherited = false)] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.compilerfeaturerequiredattribute")] #if PolyPublic public #endif @@ -28,6 +27,7 @@ sealed class CompilerFeatureRequiredAttribute : /// /// Initialize a new instance of /// + /// The name of the required compiler feature. public CompilerFeatureRequiredAttribute(string featureName) => FeatureName = featureName; diff --git a/src/Polyfill/DisableRuntimeMarshallingAttribute.cs b/src/Polyfill/DisableRuntimeMarshallingAttribute.cs index b5b6429d..34b36cbd 100644 --- a/src/Polyfill/DisableRuntimeMarshallingAttribute.cs +++ b/src/Polyfill/DisableRuntimeMarshallingAttribute.cs @@ -1,14 +1,12 @@ -#if !NET7_0_OR_GREATER +// -#pragma warning disable +#if !NET7_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; - -namespace System.Runtime.CompilerServices; +using Link = System.ComponentModel.DescriptionAttribute; /// /// Disables the built-in runtime managed/unmanaged marshalling subsystem for @@ -30,6 +28,7 @@ namespace System.Runtime.CompilerServices; [ExcludeFromCodeCoverage] [DebuggerNonUserCode] [AttributeUsage(AttributeTargets.Assembly)] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.disableruntimemarshallingattribute")] #if PolyPublic public #endif diff --git a/src/Polyfill/EnumPolyfill.cs b/src/Polyfill/EnumPolyfill.cs new file mode 100644 index 00000000..c7beb00a --- /dev/null +++ b/src/Polyfill/EnumPolyfill.cs @@ -0,0 +1,47 @@ +// + +#pragma warning disable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; + +[ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +static partial class EnumPolyfill +{ + /// + /// Retrieves an array of the values of the constants in a specified enumeration type. + /// + /// An array that contains the values of the constants in TEnum. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.enum.getvalues")] + public static TEnum[] GetValues() + where TEnum : struct, Enum + { +#if NETCOREAPPX || NETFRAMEWORK || NETSTANDARD + var values = Enum.GetValues(typeof(TEnum)); + var result = new TEnum[values.Length]; + Array.Copy(values, result, values.Length); + return result; +#else + return Enum.GetValues(); +#endif + } + /// + /// Retrieves an array of the names of the constants in a specified enumeration type. + /// + /// A string array of the names of the constants in TEnum. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.enum.getnames")] + public static string[] GetNames() + where TEnum : struct, Enum + { +#if NETCOREAPPX || NETFRAMEWORK || NETSTANDARD + return Enum.GetNames(typeof(TEnum)); +#else + return Enum.GetNames(); +#endif + } +} diff --git a/src/Polyfill/ExperimentalAttribute.cs b/src/Polyfill/ExperimentalAttribute.cs new file mode 100644 index 00000000..87cd4376 --- /dev/null +++ b/src/Polyfill/ExperimentalAttribute.cs @@ -0,0 +1,72 @@ +// + +#nullable enable + +#if !NET8_0_OR_GREATER + +namespace System.Diagnostics.CodeAnalysis; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; + +/// +/// Indicates that a parameter captures the expression passed for another parameter as a string. +/// +/// +/// Indicates that an API is experimental and it may change in the future. +/// +/// +/// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental +/// feature is used. Authors can use this attribute to ship preview features in their assemblies. +/// +[AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, Inherited = false)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0")] +#if PolyPublic +public +#endif +sealed class ExperimentalAttribute : Attribute +{ + /// + /// Initializes a new instance of the class, specifying the ID that the compiler will use + /// when reporting a use of the API the attribute applies to. + /// + /// The ID that the compiler will use when reporting a use of the API the attribute applies to. + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + /// + /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. + /// + /// The unique diagnostic ID. + /// + /// The diagnostic ID is shown in build output for warnings and errors. + /// This property represents the unique ID that can be used to suppress the warnings or errors, if needed. + /// + public string DiagnosticId { get; } + + /// + /// Gets or sets the URL for corresponding documentation. + /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. + /// + /// The format string that represents a URL to corresponding documentation. + /// An example format string is https://contoso.com/obsoletion-warnings/{0}. + public string? UrlFormat { get; set; } +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/IndexRange/Index.cs b/src/Polyfill/IndexRange/Index.cs index 9cf0638a..51910924 100644 --- a/src/Polyfill/IndexRange/Index.cs +++ b/src/Polyfill/IndexRange/Index.cs @@ -1,15 +1,13 @@ +// + #if (NET46X && VALUETUPLEREFERENCED) || NET47X || NET48X || NETSTANDARD2_0 || NETCOREAPP2X -#pragma warning disable +namespace System; -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace System; - /// Represent a type can be used to index a collection either from the start or the end. /// /// Index is used by the C# compiler to support the new index syntax diff --git a/src/Polyfill/IndexRange/Range.cs b/src/Polyfill/IndexRange/Range.cs index 7e1e9a07..1c59e07d 100644 --- a/src/Polyfill/IndexRange/Range.cs +++ b/src/Polyfill/IndexRange/Range.cs @@ -1,16 +1,13 @@ -#if (NET46X && VALUETUPLEREFERENCED) || NET47X || NET48X || NETSTANDARD2_0 || NETCOREAPP2X +// -#pragma warning disable +#if (NET46X && VALUETUPLEREFERENCED) || NET47X || NET48X || NETSTANDARD2_0 || NETCOREAPP2X -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace System; - /// Represent a range has start and end indexes. /// /// Range is used by the C# compiler to support the range syntax. diff --git a/src/Polyfill/IsExternalInit.cs b/src/Polyfill/IsExternalInit.cs index efad1be1..35a7e7c4 100644 --- a/src/Polyfill/IsExternalInit.cs +++ b/src/Polyfill/IsExternalInit.cs @@ -1,15 +1,12 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.CompilerServices; - /// /// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code. /// diff --git a/src/Polyfill/ModuleInitializerAttribute.cs b/src/Polyfill/ModuleInitializerAttribute.cs index 7bcd75ac..df30f2d3 100644 --- a/src/Polyfill/ModuleInitializerAttribute.cs +++ b/src/Polyfill/ModuleInitializerAttribute.cs @@ -1,14 +1,12 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; - -namespace System.Runtime.CompilerServices; +using Link = System.ComponentModel.DescriptionAttribute; /// /// Used to indicate to the compiler that a method should be called @@ -30,6 +28,7 @@ namespace System.Runtime.CompilerServices; /// The specification for module initializers in the .NET runtime can be found here: /// https://github.com/dotnet/runtime/blob/master/docs/design/specs/Ecma-335-Augments.md#module-initializer /// +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-7.0")] [ExcludeFromCodeCoverage] [DebuggerNonUserCode] [AttributeUsage( diff --git a/src/Polyfill/Nullability/NullabilityInfo.cs b/src/Polyfill/Nullability/NullabilityInfo.cs new file mode 100644 index 00000000..54888271 --- /dev/null +++ b/src/Polyfill/Nullability/NullabilityInfo.cs @@ -0,0 +1,88 @@ +// + +#if !NET6_0_OR_GREATER + +#nullable enable + +using System.Linq; +using System.Diagnostics.CodeAnalysis; + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; + +namespace System.Reflection +{ + using System.Linq; + using System.Diagnostics.CodeAnalysis; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + + /// + /// A class that represents nullability info + /// + [ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +sealed class NullabilityInfo + { + internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState writeState, + NullabilityInfo? elementType, NullabilityInfo[] typeArguments) + { + Type = type; + ReadState = readState; + WriteState = writeState; + ElementType = elementType; + GenericTypeArguments = typeArguments; + } + + /// + /// The of the member or generic parameter + /// to which this NullabilityInfo belongs + /// + public Type Type { get; } + /// + /// The nullability read state of the member + /// + public NullabilityState ReadState { get; internal set; } + /// + /// The nullability write state of the member + /// + public NullabilityState WriteState { get; internal set; } + /// + /// If the member type is an array, gives the of the elements of the array, null otherwise + /// + public NullabilityInfo? ElementType { get; } + /// + /// If the member type is a generic type, gives the array of for each type parameter + /// + public NullabilityInfo[] GenericTypeArguments { get; } + } + + /// + /// An enum that represents nullability state + /// + #if PolyPublic +public +#endif +enum NullabilityState + { + /// + /// Nullability context not enabled (oblivious) + /// + Unknown, + /// + /// Non nullable value or reference type + /// + NotNull, + /// + /// Nullable value or reference type + /// + Nullable + } +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/Nullability/NullabilityInfoContext.cs b/src/Polyfill/Nullability/NullabilityInfoContext.cs new file mode 100644 index 00000000..764bdbed --- /dev/null +++ b/src/Polyfill/Nullability/NullabilityInfoContext.cs @@ -0,0 +1,688 @@ +// + +#if !NET6_0_OR_GREATER + +#nullable enable + +using System.Linq; +using System.Diagnostics.CodeAnalysis; + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; + +namespace System.Reflection +{ + using System.Linq; + using System.Diagnostics.CodeAnalysis; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + + /// + /// Provides APIs for populating nullability information/context from reflection members: + /// , , and . + /// + [ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +sealed class NullabilityInfoContext + { + private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices"; + private readonly Dictionary _publicOnlyModules = new(); + private readonly Dictionary _context = new(); + + internal static bool IsSupported { get; } = + AppContext.TryGetSwitch("System.Reflection.NullabilityInfoContext.IsSupported", out bool isSupported) ? isSupported : true; + + [Flags] + private enum NotAnnotatedStatus + { + None = 0x0, // no restriction, all members annotated + Private = 0x1, // private members not annotated + Internal = 0x2 // internal members not annotated + } + + private NullabilityState? GetNullableContext(MemberInfo? memberInfo) + { + while (memberInfo != null) + { + if (_context.TryGetValue(memberInfo, out NullabilityState state)) + { + return state; + } + + foreach (CustomAttributeData attribute in memberInfo.GetCustomAttributesData()) + { + if (attribute.AttributeType.Name == "NullableContextAttribute" && + attribute.AttributeType.Namespace == CompilerServicesNameSpace && + attribute.ConstructorArguments.Count == 1) + { + state = TranslateByte(attribute.ConstructorArguments[0].Value); + _context.Add(memberInfo, state); + return state; + } + } + + memberInfo = memberInfo.DeclaringType; + } + + return null; + } + + /// + /// Populates for the given . + /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's + /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. + /// + /// The parameter which nullability info gets populated + /// If the parameterInfo parameter is null + /// + public NullabilityInfo Create(ParameterInfo parameterInfo) + { + + EnsureIsSupported(); + + IList attributes = parameterInfo.GetCustomAttributesData(); + NullableAttributeStateParser parser = parameterInfo.Member is MethodBase method && IsPrivateOrInternalMethodAndAnnotationDisabled(method) + ? NullableAttributeStateParser.Unknown + : CreateParser(attributes); + NullabilityInfo nullability = GetNullabilityInfo(parameterInfo.Member, parameterInfo.ParameterType, parser); + + if (nullability.ReadState != NullabilityState.Unknown) + { + CheckParameterMetadataType(parameterInfo, nullability); + } + + CheckNullabilityAttributes(nullability, attributes); + return nullability; + } + + private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo nullability) + { + ParameterInfo? metaParameter; + MemberInfo metaMember; + + switch (parameter.Member) + { + case ConstructorInfo ctor: + var metaCtor = (ConstructorInfo)GetMemberMetadataDefinition(ctor); + metaMember = metaCtor; + metaParameter = GetMetaParameter(metaCtor, parameter); + break; + + case MethodInfo method: + MethodInfo metaMethod = GetMethodMetadataDefinition(method); + metaMember = metaMethod; + metaParameter = string.IsNullOrEmpty(parameter.Name) ? metaMethod.ReturnParameter : GetMetaParameter(metaMethod, parameter); + break; + + default: + return; + } + + if (metaParameter != null) + { + CheckGenericParameters(nullability, metaMember, metaParameter.ParameterType, parameter.Member.ReflectedType); + } + } + + private static ParameterInfo? GetMetaParameter(MethodBase metaMethod, ParameterInfo parameter) + { + var parameters = metaMethod.GetParameters(); + for (int i = 0; i < parameters.Length; i++) + { + if (parameter.Position == i && + parameter.Name == parameters[i].Name) + { + return parameters[i]; + } + } + + return null; + } + private static MethodInfo GetMethodMetadataDefinition(MethodInfo method) + { + if (method.IsGenericMethod && !method.IsGenericMethodDefinition) + { + method = method.GetGenericMethodDefinition(); + } + + return (MethodInfo)GetMemberMetadataDefinition(method); + } + + private static void CheckNullabilityAttributes(NullabilityInfo nullability, IList attributes) + { + var codeAnalysisReadState = NullabilityState.Unknown; + var codeAnalysisWriteState = NullabilityState.Unknown; + + foreach (CustomAttributeData attribute in attributes) + { + if (attribute.AttributeType.Namespace == "System.Diagnostics.CodeAnalysis") + { + if (attribute.AttributeType.Name == "NotNullAttribute") + { + codeAnalysisReadState = NullabilityState.NotNull; + } + else if ((attribute.AttributeType.Name == "MaybeNullAttribute" || + attribute.AttributeType.Name == "MaybeNullWhenAttribute") && + codeAnalysisReadState == NullabilityState.Unknown && + !IsValueTypeOrValueTypeByRef(nullability.Type)) + { + codeAnalysisReadState = NullabilityState.Nullable; + } + else if (attribute.AttributeType.Name == "DisallowNullAttribute") + { + codeAnalysisWriteState = NullabilityState.NotNull; + } + else if (attribute.AttributeType.Name == "AllowNullAttribute" && + codeAnalysisWriteState == NullabilityState.Unknown && + !IsValueTypeOrValueTypeByRef(nullability.Type)) + { + codeAnalysisWriteState = NullabilityState.Nullable; + } + } + } + + if (codeAnalysisReadState != NullabilityState.Unknown) + { + nullability.ReadState = codeAnalysisReadState; + } + if (codeAnalysisWriteState != NullabilityState.Unknown) + { + nullability.WriteState = codeAnalysisWriteState; + } + } + + /// + /// Populates for the given . + /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's + /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. + /// + /// The parameter which nullability info gets populated + /// If the propertyInfo parameter is null + /// + public NullabilityInfo Create(PropertyInfo propertyInfo) + { + + EnsureIsSupported(); + + MethodInfo? getter = propertyInfo.GetGetMethod(true); + MethodInfo? setter = propertyInfo.GetSetMethod(true); + bool annotationsDisabled = (getter == null || IsPrivateOrInternalMethodAndAnnotationDisabled(getter)) + && (setter == null || IsPrivateOrInternalMethodAndAnnotationDisabled(setter)); + NullableAttributeStateParser parser = annotationsDisabled ? NullableAttributeStateParser.Unknown : CreateParser(propertyInfo.GetCustomAttributesData()); + NullabilityInfo nullability = GetNullabilityInfo(propertyInfo, propertyInfo.PropertyType, parser); + + if (getter != null) + { + CheckNullabilityAttributes(nullability, getter.ReturnParameter.GetCustomAttributesData()); + } + else + { + nullability.ReadState = NullabilityState.Unknown; + } + + if (setter != null) + { + var parameters = setter.GetParameters(); + + CheckNullabilityAttributes(nullability, parameters[parameters.Length-1].GetCustomAttributesData()); + } + else + { + nullability.WriteState = NullabilityState.Unknown; + } + + return nullability; + } + + private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method) + { + if ((method.IsPrivate || method.IsFamilyAndAssembly || method.IsAssembly) && + IsPublicOnly(method.IsPrivate, method.IsFamilyAndAssembly, method.IsAssembly, method.Module)) + { + return true; + } + + return false; + } + + /// + /// Populates for the given . + /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's + /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. + /// + /// The parameter which nullability info gets populated + /// If the eventInfo parameter is null + /// + public NullabilityInfo Create(EventInfo eventInfo) + { + + EnsureIsSupported(); + + return GetNullabilityInfo(eventInfo, eventInfo.EventHandlerType!, CreateParser(eventInfo.GetCustomAttributesData())); + } + + /// + /// Populates for the given + /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's + /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. + /// + /// The parameter which nullability info gets populated + /// If the fieldInfo parameter is null + /// + public NullabilityInfo Create(FieldInfo fieldInfo) + { + + EnsureIsSupported(); + + IList attributes = fieldInfo.GetCustomAttributesData(); + NullableAttributeStateParser parser = IsPrivateOrInternalFieldAndAnnotationDisabled(fieldInfo) ? NullableAttributeStateParser.Unknown : CreateParser(attributes); + NullabilityInfo nullability = GetNullabilityInfo(fieldInfo, fieldInfo.FieldType, parser); + CheckNullabilityAttributes(nullability, attributes); + return nullability; + } + + private static void EnsureIsSupported() + { + if (!IsSupported) + { + throw new InvalidOperationException("NullabilityInfoContext is not supported"); + } + } + + private bool IsPrivateOrInternalFieldAndAnnotationDisabled(FieldInfo fieldInfo) + { + if ((fieldInfo.IsPrivate || fieldInfo.IsFamilyAndAssembly || fieldInfo.IsAssembly) && + IsPublicOnly(fieldInfo.IsPrivate, fieldInfo.IsFamilyAndAssembly, fieldInfo.IsAssembly, fieldInfo.Module)) + { + return true; + } + + return false; + } + + private bool IsPublicOnly(bool isPrivate, bool isFamilyAndAssembly, bool isAssembly, Module module) + { + if (!_publicOnlyModules.TryGetValue(module, out NotAnnotatedStatus value)) + { + value = PopulateAnnotationInfo(module.GetCustomAttributesData()); + _publicOnlyModules.Add(module, value); + } + + if (value == NotAnnotatedStatus.None) + { + return false; + } + + if ((isPrivate || isFamilyAndAssembly) && value.HasFlag(NotAnnotatedStatus.Private) || + isAssembly && value.HasFlag(NotAnnotatedStatus.Internal)) + { + return true; + } + + return false; + } + + private static NotAnnotatedStatus PopulateAnnotationInfo(IList customAttributes) + { + foreach (CustomAttributeData attribute in customAttributes) + { + if (attribute.AttributeType.Name == "NullablePublicOnlyAttribute" && + attribute.AttributeType.Namespace == CompilerServicesNameSpace && + attribute.ConstructorArguments.Count == 1) + { + if (attribute.ConstructorArguments[0].Value is bool boolValue && boolValue) + { + return NotAnnotatedStatus.Internal | NotAnnotatedStatus.Private; + } + else + { + return NotAnnotatedStatus.Private; + } + } + } + + return NotAnnotatedStatus.None; + } + + private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser) + { + int index = 0; + NullabilityInfo nullability = GetNullabilityInfo(memberInfo, type, parser, ref index); + + if (nullability.ReadState != NullabilityState.Unknown) + { + TryLoadGenericMetaTypeNullability(memberInfo, nullability); + } + + return nullability; + } + + private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser, ref int index) + { + NullabilityState state = NullabilityState.Unknown; + NullabilityInfo? elementState = null; + NullabilityInfo[] genericArgumentsState = Array.Empty(); + Type underlyingType = type; + + if (underlyingType.IsByRef || underlyingType.IsPointer) + { + underlyingType = underlyingType.GetElementType()!; + } + + if (underlyingType.IsValueType) + { + if (Nullable.GetUnderlyingType(underlyingType) is { } nullableUnderlyingType) + { + underlyingType = nullableUnderlyingType; + state = NullabilityState.Nullable; + } + else + { + state = NullabilityState.NotNull; + } + + if (underlyingType.IsGenericType) + { + ++index; + } + } + else + { + if (!parser.ParseNullableState(index++, ref state) + && GetNullableContext(memberInfo) is { } contextState) + { + state = contextState; + } + + if (underlyingType.IsArray) + { + elementState = GetNullabilityInfo(memberInfo, underlyingType.GetElementType()!, parser, ref index); + } + } + + if (underlyingType.IsGenericType) + { + Type[] genericArguments = underlyingType.GetGenericArguments(); + genericArgumentsState = new NullabilityInfo[genericArguments.Length]; + + for (int i = 0; i < genericArguments.Length; i++) + { + genericArgumentsState[i] = GetNullabilityInfo(memberInfo, genericArguments[i], parser, ref index); + } + } + + return new NullabilityInfo(type, state, state, elementState, genericArgumentsState); + } + + private static NullableAttributeStateParser CreateParser(IList customAttributes) + { + foreach (CustomAttributeData attribute in customAttributes) + { + if (attribute.AttributeType.Name == "NullableAttribute" && + attribute.AttributeType.Namespace == CompilerServicesNameSpace && + attribute.ConstructorArguments.Count == 1) + { + return new NullableAttributeStateParser(attribute.ConstructorArguments[0].Value); + } + } + + return new NullableAttributeStateParser(null); + } + + private void TryLoadGenericMetaTypeNullability(MemberInfo memberInfo, NullabilityInfo nullability) + { + MemberInfo? metaMember = GetMemberMetadataDefinition(memberInfo); + Type? metaType = null; + if (metaMember is FieldInfo field) + { + metaType = field.FieldType; + } + else if (metaMember is PropertyInfo property) + { + metaType = GetPropertyMetaType(property); + } + + if (metaType != null) + { + CheckGenericParameters(nullability, metaMember!, metaType, memberInfo.ReflectedType); + } + } + + private static MemberInfo GetMemberMetadataDefinition(MemberInfo member) + { + Type? type = member.DeclaringType; + if ((type != null) && type.IsGenericType && !type.IsGenericTypeDefinition) + { + return type.GetGenericTypeDefinition().GetMemberWithSameMetadataDefinitionAs(member); + } + + return member; + } + + private static Type GetPropertyMetaType(PropertyInfo property) + { + if (property.GetGetMethod(true) is MethodInfo method) + { + return method.ReturnType; + } + + return property.GetSetMethod(true)!.GetParameters()[0].ParameterType; + } + + private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo metaMember, Type metaType, Type? reflectedType) + { + if (metaType.IsGenericParameter) + { + if (nullability.ReadState == NullabilityState.NotNull) + { + TryUpdateGenericParameterNullability(nullability, metaType, reflectedType); + } + } + else if (metaType.ContainsGenericParameters) + { + if (nullability.GenericTypeArguments.Length > 0) + { + Type[] genericArguments = metaType.GetGenericArguments(); + + for (int i = 0; i < genericArguments.Length; i++) + { + CheckGenericParameters(nullability.GenericTypeArguments[i], metaMember, genericArguments[i], reflectedType); + } + } + else if (nullability.ElementType is { } elementNullability && metaType.IsArray) + { + CheckGenericParameters(elementNullability, metaMember, metaType.GetElementType()!, reflectedType); + } + // We could also follow this branch for metaType.IsPointer, but since pointers must be unmanaged this + // will be a no-op regardless + else if (metaType.IsByRef) + { + CheckGenericParameters(nullability, metaMember, metaType.GetElementType()!, reflectedType); + } + } + } + + private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, Type genericParameter, Type? reflectedType) + { + Debug.Assert(genericParameter.IsGenericParameter); + + if (reflectedType is not null + && !genericParameter.IsGenericMethodParameter() + && TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType)) + { + return true; + } + + if (IsValueTypeOrValueTypeByRef(nullability.Type)) + { + return true; + } + + var state = NullabilityState.Unknown; + if (CreateParser(genericParameter.GetCustomAttributesData()).ParseNullableState(0, ref state)) + { + nullability.ReadState = state; + nullability.WriteState = state; + return true; + } + + if (GetNullableContext(genericParameter) is { } contextState) + { + nullability.ReadState = contextState; + nullability.WriteState = contextState; + return true; + } + + return false; + } + + private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType) + { + Debug.Assert(genericParameter.IsGenericParameter && !genericParameter.IsGenericMethodParameter()); + + Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context; + if (genericParameter.DeclaringType == contextTypeDefinition) + { + return false; + } + + Type? baseType = contextTypeDefinition.BaseType; + if (baseType is null) + { + return false; + } + + if (!baseType.IsGenericType + || (baseType.IsGenericTypeDefinition ? baseType : baseType.GetGenericTypeDefinition()) != genericParameter.DeclaringType) + { + return TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, baseType, reflectedType); + } + + Type[] genericArguments = baseType.GetGenericArguments(); + Type genericArgument = genericArguments[genericParameter.GenericParameterPosition]; + if (genericArgument.IsGenericParameter) + { + return TryUpdateGenericParameterNullability(nullability, genericArgument, reflectedType); + } + + NullableAttributeStateParser parser = CreateParser(contextTypeDefinition.GetCustomAttributesData()); + int nullabilityStateIndex = 1; // start at 1 since index 0 is the type itself + for (int i = 0; i < genericParameter.GenericParameterPosition; i++) + { + nullabilityStateIndex += CountNullabilityStates(genericArguments[i]); + } + return TryPopulateNullabilityInfo(nullability, parser, ref nullabilityStateIndex); + + static int CountNullabilityStates(Type type) + { + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + if (underlyingType.IsGenericType) + { + int count = 1; + foreach (Type genericArgument in underlyingType.GetGenericArguments()) + { + count += CountNullabilityStates(genericArgument); + } + return count; + } + + if (underlyingType.HasElementType) + { + return (underlyingType.IsArray ? 1 : 0) + CountNullabilityStates(underlyingType.GetElementType()!); + } + + return type.IsValueType ? 0 : 1; + } + } + + private static bool TryPopulateNullabilityInfo(NullabilityInfo nullability, NullableAttributeStateParser parser, ref int index) + { + bool isValueType = IsValueTypeOrValueTypeByRef(nullability.Type); + if (!isValueType) + { + var state = NullabilityState.Unknown; + if (!parser.ParseNullableState(index, ref state)) + { + return false; + } + + nullability.ReadState = state; + nullability.WriteState = state; + } + + if (!isValueType || (Nullable.GetUnderlyingType(nullability.Type) ?? nullability.Type).IsGenericType) + { + index++; + } + + if (nullability.GenericTypeArguments.Length > 0) + { + foreach (NullabilityInfo genericTypeArgumentNullability in nullability.GenericTypeArguments) + { + TryPopulateNullabilityInfo(genericTypeArgumentNullability, parser, ref index); + } + } + else if (nullability.ElementType is { } elementTypeNullability) + { + TryPopulateNullabilityInfo(elementTypeNullability, parser, ref index); + } + + return true; + } + + private static NullabilityState TranslateByte(object? value) + { + return value is byte b ? TranslateByte(b) : NullabilityState.Unknown; + } + + private static NullabilityState TranslateByte(byte b) => + b switch + { + 1 => NullabilityState.NotNull, + 2 => NullabilityState.Nullable, + _ => NullabilityState.Unknown + }; + + private static bool IsValueTypeOrValueTypeByRef(Type type) => + type.IsValueType || ((type.IsByRef || type.IsPointer) && type.GetElementType()!.IsValueType); + + private readonly struct NullableAttributeStateParser + { + private static readonly object UnknownByte = (byte)0; + + private readonly object? _nullableAttributeArgument; + + public NullableAttributeStateParser(object? nullableAttributeArgument) + { + this._nullableAttributeArgument = nullableAttributeArgument; + } + + public static NullableAttributeStateParser Unknown => new(UnknownByte); + + public bool ParseNullableState(int index, ref NullabilityState state) + { + switch (this._nullableAttributeArgument) + { + case byte b: + state = TranslateByte(b); + return true; + case ReadOnlyCollection args + when index < args.Count && args[index].Value is byte elementB: + state = TranslateByte(elementB); + return true; + default: + return false; + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/Nullability/NullabilityInfoExtensions.cs b/src/Polyfill/Nullability/NullabilityInfoExtensions.cs new file mode 100644 index 00000000..66c188b1 --- /dev/null +++ b/src/Polyfill/Nullability/NullabilityInfoExtensions.cs @@ -0,0 +1,146 @@ +// + +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Reflection; + +/// +/// Static and thread safe wrapper around NullabilityInfoContext. +/// +#if PolyPublic +public +#endif +static partial class Polyfill +{ + static ConcurrentDictionary parameterCache = []; + static ConcurrentDictionary propertyCache = []; + static ConcurrentDictionary eventCache = []; + static ConcurrentDictionary fieldCache = []; + + public static NullabilityInfo GetNullabilityInfo(this MemberInfo info) + { + if (info is PropertyInfo propertyInfo) + { + return propertyInfo.GetNullabilityInfo(); + } + + if (info is EventInfo eventInfo) + { + return eventInfo.GetNullabilityInfo(); + } + + if (info is FieldInfo fieldInfo) + { + return fieldInfo.GetNullabilityInfo(); + } + + throw new ArgumentException($"Unsupported type:{info.GetType().FullName}"); + } + + public static NullabilityState GetNullability(this MemberInfo info) => + GetReadOrWriteState(info.GetNullabilityInfo()); + + public static bool IsNullable(this MemberInfo info) + { + var nullability = info.GetNullabilityInfo(); + return IsNullable(info.Name, nullability); + } + + public static NullabilityInfo GetNullabilityInfo(this FieldInfo info) => + fieldCache.GetOrAdd(info, inner => + { + var context = new NullabilityInfoContext(); + return context.Create(inner); + }); + + public static NullabilityState GetNullability(this FieldInfo info) => + GetReadOrWriteState(info.GetNullabilityInfo()); + + public static bool IsNullable(this FieldInfo info) + { + var nullability = info.GetNullabilityInfo(); + return IsNullable(info.Name, nullability); + } + + public static NullabilityInfo GetNullabilityInfo(this EventInfo info) => + eventCache.GetOrAdd(info, inner => + { + var context = new NullabilityInfoContext(); + return context.Create(inner); + }); + + public static NullabilityState GetNullability(this EventInfo info) => + GetReadOrWriteState(info.GetNullabilityInfo()); + + public static bool IsNullable(this EventInfo info) + { + var nullability = info.GetNullabilityInfo(); + return IsNullable(info.Name, nullability); + } + + public static NullabilityInfo GetNullabilityInfo(this PropertyInfo info) => + propertyCache.GetOrAdd( + info, + inner => + { + var context = new NullabilityInfoContext(); + return context.Create(inner); + }); + + public static NullabilityState GetNullability(this PropertyInfo info) => + GetReadOrWriteState(info.GetNullabilityInfo()); + + public static bool IsNullable(this PropertyInfo info) + { + var nullability = info.GetNullabilityInfo(); + return IsNullable(info.Name, nullability); + } + + public static NullabilityInfo GetNullabilityInfo(this ParameterInfo info) => + parameterCache.GetOrAdd(info, inner => + { + var context = new NullabilityInfoContext(); + return context.Create(inner); + }); + + public static NullabilityState GetNullability(this ParameterInfo info) => + GetReadOrWriteState(info.GetNullabilityInfo()); + + public static bool IsNullable(this ParameterInfo info) + { + var nullability = info.GetNullabilityInfo(); + return IsNullable(info.Name!, nullability); + } + + static NullabilityState GetReadOrWriteState(NullabilityInfo nullability) + { + if (nullability.ReadState == NullabilityState.Unknown) + { + return nullability.WriteState; + } + + return nullability.ReadState; + } + + static NullabilityState GetKnownState(string name, NullabilityInfo nullability) + { + var readState = nullability.ReadState; + if (readState != NullabilityState.Unknown) + { + return readState; + } + + var writeState = nullability.WriteState; + if (writeState != NullabilityState.Unknown) + { + return writeState; + } + + throw new($"The nullability of '{nullability.Type.FullName}.{name}' is unknown. Assembly: {nullability.Type.Assembly.FullName}."); + } + + static bool IsNullable(string name, NullabilityInfo nullability) => + GetKnownState(name, nullability) == NullabilityState.Nullable; +} \ No newline at end of file diff --git a/src/Polyfill/Nullable/AllowNullAttribute.cs b/src/Polyfill/Nullable/AllowNullAttribute.cs index b91598df..9035fdbe 100644 --- a/src/Polyfill/Nullable/AllowNullAttribute.cs +++ b/src/Polyfill/Nullable/AllowNullAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/DisallowNullAttribute.cs b/src/Polyfill/Nullable/DisallowNullAttribute.cs index 4a0fe5c9..f609a2d0 100644 --- a/src/Polyfill/Nullable/DisallowNullAttribute.cs +++ b/src/Polyfill/Nullable/DisallowNullAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/DoesNotReturnAttribute.cs b/src/Polyfill/Nullable/DoesNotReturnAttribute.cs index bed703f1..38641474 100644 --- a/src/Polyfill/Nullable/DoesNotReturnAttribute.cs +++ b/src/Polyfill/Nullable/DoesNotReturnAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/DoesNotReturnIfAttribute.cs b/src/Polyfill/Nullable/DoesNotReturnIfAttribute.cs index d2928bab..05d0d779 100644 --- a/src/Polyfill/Nullable/DoesNotReturnIfAttribute.cs +++ b/src/Polyfill/Nullable/DoesNotReturnIfAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/MaybeNullAttribute.cs b/src/Polyfill/Nullable/MaybeNullAttribute.cs index 3464386b..e1f077d2 100644 --- a/src/Polyfill/Nullable/MaybeNullAttribute.cs +++ b/src/Polyfill/Nullable/MaybeNullAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/MaybeNullWhenAttribute.cs b/src/Polyfill/Nullable/MaybeNullWhenAttribute.cs index 7c65361e..08271024 100644 --- a/src/Polyfill/Nullable/MaybeNullWhenAttribute.cs +++ b/src/Polyfill/Nullable/MaybeNullWhenAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/MemberNotNullAttribute.cs b/src/Polyfill/Nullable/MemberNotNullAttribute.cs index 5b2b7c65..bfbde6a8 100644 --- a/src/Polyfill/Nullable/MemberNotNullAttribute.cs +++ b/src/Polyfill/Nullable/MemberNotNullAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX namespace System.Diagnostics.CodeAnalysis; @@ -38,7 +35,7 @@ sealed class MemberNotNullAttribute : /// The field or property member that is promised to be not-null. /// public MemberNotNullAttribute(string member) => - Members = new[] { member }; + Members = [member]; /// /// Initializes the attribute with the list of field and property members. diff --git a/src/Polyfill/Nullable/MemberNotNullWhenAttribute.cs b/src/Polyfill/Nullable/MemberNotNullWhenAttribute.cs index ea9e1be7..b9fbd538 100644 --- a/src/Polyfill/Nullable/MemberNotNullWhenAttribute.cs +++ b/src/Polyfill/Nullable/MemberNotNullWhenAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD || NETFRAMEWORK || NETCOREAPPX namespace System.Diagnostics.CodeAnalysis; @@ -49,7 +46,7 @@ sealed class MemberNotNullWhenAttribute : public MemberNotNullWhenAttribute(bool returnValue, string member) { ReturnValue = returnValue; - Members = new[] { member }; + Members = [member]; } /// diff --git a/src/Polyfill/Nullable/NotNullAttribute.cs b/src/Polyfill/Nullable/NotNullAttribute.cs index 682aee5a..e468a877 100644 --- a/src/Polyfill/Nullable/NotNullAttribute.cs +++ b/src/Polyfill/Nullable/NotNullAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/NotNullIfNotNullAttribute.cs b/src/Polyfill/Nullable/NotNullIfNotNullAttribute.cs index 96f8be0f..621fb50d 100644 --- a/src/Polyfill/Nullable/NotNullIfNotNullAttribute.cs +++ b/src/Polyfill/Nullable/NotNullIfNotNullAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Nullable/NotNullWhenAttribute.cs b/src/Polyfill/Nullable/NotNullWhenAttribute.cs index 8e775192..6136e494 100644 --- a/src/Polyfill/Nullable/NotNullWhenAttribute.cs +++ b/src/Polyfill/Nullable/NotNullWhenAttribute.cs @@ -1,9 +1,6 @@ -#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/PlatformCompatibility/OSPlatformAttribute.cs b/src/Polyfill/PlatformCompatibility/OSPlatformAttribute.cs index f792a846..048f7421 100644 --- a/src/Polyfill/PlatformCompatibility/OSPlatformAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/OSPlatformAttribute.cs @@ -1,15 +1,14 @@ -#if !NET7_0_OR_GREATER +// + +#if !NET7_0_OR_GREATER #pragma warning disable -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +namespace System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.Versioning; - /// /// Base type for all platform-specific API attributes. /// diff --git a/src/Polyfill/PlatformCompatibility/ObsoletedOSPlatformAttribute.cs b/src/Polyfill/PlatformCompatibility/ObsoletedOSPlatformAttribute.cs index 6d8e14a0..2854ca96 100644 --- a/src/Polyfill/PlatformCompatibility/ObsoletedOSPlatformAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/ObsoletedOSPlatformAttribute.cs @@ -1,17 +1,16 @@ -#if !NET7_0_OR_GREATER +// -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +#if !NET7_0_OR_GREATER -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +#pragma warning disable #nullable enable namespace System.Runtime.Versioning; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + using Targets = AttributeTargets; /// diff --git a/src/Polyfill/PlatformCompatibility/SupportedOSPlatformAttribute.cs b/src/Polyfill/PlatformCompatibility/SupportedOSPlatformAttribute.cs index 7f3507ec..11655f12 100644 --- a/src/Polyfill/PlatformCompatibility/SupportedOSPlatformAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/SupportedOSPlatformAttribute.cs @@ -1,15 +1,14 @@ -#if !NET5_0_OR_GREATER +// + +#if !NET5_0_OR_GREATER #pragma warning disable -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +namespace System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.Versioning; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/PlatformCompatibility/SupportedOSPlatformGuardAttribute.cs b/src/Polyfill/PlatformCompatibility/SupportedOSPlatformGuardAttribute.cs index bcc74900..f8d109e1 100644 --- a/src/Polyfill/PlatformCompatibility/SupportedOSPlatformGuardAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/SupportedOSPlatformGuardAttribute.cs @@ -1,15 +1,14 @@ -#if !NET6_0_OR_GREATER +// + +#if !NET6_0_OR_GREATER #pragma warning disable -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +namespace System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.Versioning; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/PlatformCompatibility/TargetPlatformAttribute.cs b/src/Polyfill/PlatformCompatibility/TargetPlatformAttribute.cs index 6830d290..d340b45c 100644 --- a/src/Polyfill/PlatformCompatibility/TargetPlatformAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/TargetPlatformAttribute.cs @@ -1,15 +1,12 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +namespace System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.Versioning; - /// /// Records the platform that the project targeted. /// diff --git a/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformAttribute.cs b/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformAttribute.cs index 8c2b0585..ffc26bef 100644 --- a/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformAttribute.cs @@ -1,17 +1,14 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable +#if !NET5_0_OR_GREATER #nullable enable -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +namespace System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.Versioning; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformGuardAttribute.cs b/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformGuardAttribute.cs index 57be1a25..21835579 100644 --- a/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformGuardAttribute.cs +++ b/src/Polyfill/PlatformCompatibility/UnsupportedOSPlatformGuardAttribute.cs @@ -1,15 +1,14 @@ +// + #if !NET6_0_OR_GREATER #pragma warning disable -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart +namespace System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.Versioning; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/Polyfill.cs b/src/Polyfill/Polyfill.cs new file mode 100644 index 00000000..3ae77888 --- /dev/null +++ b/src/Polyfill/Polyfill.cs @@ -0,0 +1,15 @@ +// + +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +[EditorBrowsable(EditorBrowsableState.Never)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyPublic +public +#endif +static partial class Polyfill +{ +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill.csproj b/src/Polyfill/Polyfill.csproj index c308d80f..b34a5e3e 100644 --- a/src/Polyfill/Polyfill.csproj +++ b/src/Polyfill/Polyfill.csproj @@ -1,12 +1,11 @@ - + netstandard2.0 Polyfill.nuspec Source only packages that exposes newer .net and C# features to older runtimes. - - + @@ -16,5 +15,4 @@ configuration=$(Configuration);version=$(PackageVersion);authors=$(Authors);projectUrl=$(PackageProjectUrl);description=$(Description);tags=$(PackageTags) - \ No newline at end of file diff --git a/src/Polyfill/Polyfill.nuspec b/src/Polyfill/Polyfill.nuspec index c737e44c..b447dd2e 100644 --- a/src/Polyfill/Polyfill.nuspec +++ b/src/Polyfill/Polyfill.nuspec @@ -12,9 +12,6 @@ $description$ $copyright$ $tags$ - - - @@ -22,14 +19,18 @@ target="contentFiles/cs/netstandard2.0/Polyfill/"/> + + + - diff --git a/src/Polyfill/Polyfill.props b/src/Polyfill/Polyfill.props deleted file mode 100644 index 78930b06..00000000 --- a/src/Polyfill/Polyfill.props +++ /dev/null @@ -1,42 +0,0 @@ - - - $(DefineConstants);AllowUnsafeBlocks - - - $(DefineConstants);PolyPublic - - - false - false - false - $(TargetFramework.ToLower()) - - - $(DefineConstants);NETCOREAPP2X - - - $(DefineConstants);NETCOREAPP3X - - - $(DefineConstants);NET46X - - - $(DefineConstants);NET47X - - - $(DefineConstants);NET48X - - - $(DefineConstants);NET4X - - - $(DefineConstants);NETCOREAPPX - - - - false - - - \ No newline at end of file diff --git a/src/Polyfill/Polyfill.targets b/src/Polyfill/Polyfill.targets index 94d5a590..3b785a66 100644 --- a/src/Polyfill/Polyfill.targets +++ b/src/Polyfill/Polyfill.targets @@ -1,19 +1,77 @@ - + + $(PrepareForBuildDependsOn);PreparePolyfill + false + false + false + false + $(TargetFramework.ToLower()) + + + $(DefineConstants);AllowUnsafeBlocks + + + $(DefineConstants);PolyPublic + + + $(DefineConstants);NETCOREAPP2X + + + $(DefineConstants);NETCOREAPP3X + + + $(DefineConstants);NET46X + + + $(DefineConstants);NET47X + + + $(DefineConstants);NET48X + + + $(DefineConstants);NETCOREAPPX + + true + Condition="@(PackageDependencies->WithMetadataValue('Identity', 'System.ValueTuple')->Count()) != 0">true $(DefineConstants);VALUETUPLEREFERENCED + true + Condition="@(PackageDependencies->WithMetadataValue('Identity', 'System.Memory')->Count()) != 0">true $(DefineConstants);MEMORYREFERENCED + true + Condition="@(PackageDependencies->WithMetadataValue('Identity', 'System.Threading.Tasks.Extensions')->Count()) != 0">true $(DefineConstants);TASKSEXTENSIONSREFERENCED + + true + $(DefineConstants);HTTPREFERENCED + + + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants);HAS_SPAN + $(DefineConstants) + $(DefineConstants) + + + false + + \ No newline at end of file diff --git a/src/Polyfill/PolyfillExtensions.cs b/src/Polyfill/PolyfillExtensions.cs deleted file mode 100644 index 6294c8fd..00000000 --- a/src/Polyfill/PolyfillExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -[ExcludeFromCodeCoverage] -[DebuggerNonUserCode] -#if PolyPublic -public -#endif -static partial class PolyfillExtensions -{ -} \ No newline at end of file diff --git a/src/Polyfill/PolyfillExtensions_KeyValuePair.cs b/src/Polyfill/PolyfillExtensions_KeyValuePair.cs deleted file mode 100644 index 31a6652c..00000000 --- a/src/Polyfill/PolyfillExtensions_KeyValuePair.cs +++ /dev/null @@ -1,26 +0,0 @@ -#if NET4X || NETSTANDARD2_0 - -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart -// ReSharper disable UnusedMember.Global - -using System; -using System.Collections.Generic; - -#if PolyPublic -public -#endif -static partial class PolyfillExtensions -{ - public static void Deconstruct( - this KeyValuePair pair, - out TKey key, - out TValue value) - { - key = pair.Key; - value = pair.Value; - } -} -#endif \ No newline at end of file diff --git a/src/Polyfill/PolyfillExtensions_Memory.cs b/src/Polyfill/PolyfillExtensions_Memory.cs deleted file mode 100644 index a8a1edfc..00000000 --- a/src/Polyfill/PolyfillExtensions_Memory.cs +++ /dev/null @@ -1,109 +0,0 @@ -#if MEMORYREFERENCED && (NET4X || NETSTANDARD || NETCOREAPP2X) - -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global - -using System; -using System.Text; - -#if PolyPublic -public -#endif -static partial class PolyfillExtensions -{ - public static bool Contains(this ReadOnlySpan target, char value) - { - foreach (var ch in target) - { - if (ch == value) - { - return true; - } - } - - return false; - } - - public static void Append(this StringBuilder target, ReadOnlySpan value) - { - if (value.Length <= 0) - { - return; - } - -#if AllowUnsafeBlocks - unsafe - { - fixed (char* valueChars = &System.Runtime.InteropServices.MemoryMarshal.GetReference(value)) - { - target.Append(valueChars, value.Length); - } - } -#else - target.Append(value.ToArray()); -#endif - } - - public static bool SequenceEqual(this ReadOnlySpan target, string other) - { - if (target.Length != other.Length) - { - return false; - } - - for (var index = 0; index < target.Length; index++) - { - var ch1 = target[index]; - var ch2 = other[index]; - if (ch1 != ch2) - { - return false; - } - } - - return true; - } - - public static bool Equals(this StringBuilder target, ReadOnlySpan span) - { - if (target.Length != span.Length) - { - return false; - } - - for (var index = 0; index < target.Length; index++) - { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) - { - return false; - } - } - - return true; - } - - public static bool SequenceEqual(this Span target, string other) - { - if (target.Length != other.Length) - { - return false; - } - - for (var index = 0; index < target.Length; index++) - { - var ch1 = target[index]; - var ch2 = other[index]; - if (ch1 != ch2) - { - return false; - } - } - - return true; - } -} -#endif \ No newline at end of file diff --git a/src/Polyfill/PolyfillExtensions_Stream.cs b/src/Polyfill/PolyfillExtensions_Stream.cs deleted file mode 100644 index a5e19ef3..00000000 --- a/src/Polyfill/PolyfillExtensions_Stream.cs +++ /dev/null @@ -1,83 +0,0 @@ -#if TASKSEXTENSIONSREFERENCED && (NET4X || NETSTANDARD2_0 || NETCOREAPP2_0) - -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global - -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -#if PolyPublic -public -#endif -static partial class PolyfillExtensions -{ - public static ValueTask ReadAsync( - this Stream stream, - Memory buffer, - CancellationToken cancellationToken = default) - { - if (!MemoryMarshal.TryGetArray((ReadOnlyMemory) buffer, out var segment)) - { - segment = new(buffer.ToArray()); - } - - return new(stream.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); - } - - public static ValueTask ReadAsync( - this StreamReader reader, - Memory buffer, - CancellationToken cancellationToken = default) - { - // StreamReader doesn't accept cancellation token (pre-netstd2.1) - cancellationToken.ThrowIfCancellationRequested(); - - if (!MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment)) - { - segment = new(buffer.ToArray()); - } - - return new(reader.ReadAsync(segment.Array!, segment.Offset, segment.Count)); - } - - public static ValueTask WriteAsync( - this Stream stream, - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default) - { - if (!MemoryMarshal.TryGetArray(buffer, out var segment)) - { - segment = new(buffer.ToArray()); - } - - return new(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); - } - - public static ValueTask WriteAsync( - this StreamWriter stream, - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default) - { - // StreamReader doesn't accept cancellation token (pre-netstd2.1) - cancellationToken.ThrowIfCancellationRequested(); - - if (!MemoryMarshal.TryGetArray(buffer, out var segment)) - { - segment = new(buffer.ToArray()); - } - - return new(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count)); - } - - public static Task CopyToAsync( - this Stream stream, - Stream destination, - CancellationToken cancellationToken = default) => - stream.CopyToAsync(destination, 81920, cancellationToken); -} -#endif \ No newline at end of file diff --git a/src/Polyfill/PolyfillExtensions_String.cs b/src/Polyfill/PolyfillExtensions_String.cs deleted file mode 100644 index 83d4037f..00000000 --- a/src/Polyfill/PolyfillExtensions_String.cs +++ /dev/null @@ -1,41 +0,0 @@ -#if NETFRAMEWORK || NETSTANDARD2_0 - -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable PartialTypeWithSinglePart - -using System; -using System.Text; - -#if PolyPublic -public -#endif -static partial class PolyfillExtensions -{ - public static bool Contains(this string target, string value, StringComparison comparisonType) => - target.IndexOf(value, comparisonType) >= 0; - - public static bool StartsWith(this string target, char value) - { - if (target.Length == 0) - { - return false; - } - - return target[0] == value; - } - - public static bool EndsWith(this string target, char value) - { - if (target.Length == 0) - { - return false; - } - - var lastPos = target.Length - 1; - return lastPos < target.Length && - target[lastPos] == value; - } -} -#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_CancellationToken.cs b/src/Polyfill/Polyfill_CancellationToken.cs new file mode 100644 index 00000000..2e4dc550 --- /dev/null +++ b/src/Polyfill/Polyfill_CancellationToken.cs @@ -0,0 +1,137 @@ +// + +#nullable enable + +#pragma warning disable + +using System; +using System.Threading; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + +#if !NETCOREAPP3_0_OR_GREATER + + /// + /// Registers a delegate that will be called when this + /// CancellationToken is canceled. + /// + /// + /// + /// If this token is already in the canceled state, the delegate will be run immediately and synchronously. + /// Any exception the delegate generates will be propagated out of this method call. + /// + /// + /// ExecutionContext is not captured nor flowed + /// to the callback's invocation. + /// + /// + /// The delegate to be executed when the CancellationToken is canceled. + /// The state to pass to the when the delegate is invoked. This may be null. + /// The instance that can + /// be used to unregister the callback. + /// is null. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object))-system-object)")] + public static CancellationTokenRegistration UnsafeRegister(this CancellationToken target, Action callback, object? state) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + // The main difference between UnsafeRegister and Register appears to be that UnsafeRegister callbacks don't capture and use the execution context. + // So to emulate that here, let's suppress the execution context if needed before calling Register. + // This idea was taken from UniTask and how they implemented their RegisterWithoutCaptureExecutionContext extension methods: + // https://github.com/Cysharp/UniTask/blob/master/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenExtensions.cs + + var restoreFlow = false; + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + try + { + return target.Register(callback, state, false); + } + finally + { + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + +#endif + +#if !NET6_0_OR_GREATER + + /// Registers a delegate that will be called when this CancellationToken is canceled. + /// + /// If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate + /// generates will be propagated out of this method call. The current ExecutionContext, if one exists, + /// will be captured along with the delegate and will be used when executing it. The current is not captured. + /// + /// The delegate to be executed when the CancellationToken is canceled. + /// The state to pass to the when the delegate is invoked. This may be null. + /// The instance that can be used to unregister the callback. + /// is null. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.register#system-threading-cancellationtoken-register(system-action((system-object-system-threading-cancellationtoken))-system-object)")] + public static CancellationTokenRegistration Register(this CancellationToken target, Action callback, object? state) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + return target.Register((data) => callback(data, target), state, useSynchronizationContext: false); + } + + /// Registers a delegate that will be called when this CancellationToken is canceled. + /// + /// If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate + /// generates will be propagated out of this method call. is not captured nor flowed to the callback's invocation. + /// + /// The delegate to be executed when the CancellationToken is canceled. + /// The state to pass to the when the delegate is invoked. This may be null. + /// The instance that can be used to unregister the callback. + /// is null. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.unsaferegister#system-threading-cancellationtoken-unsaferegister(system-action((system-object-system-threading-cancellationtoken))-system-object)")] + public static CancellationTokenRegistration UnsafeRegister(this CancellationToken target, Action callback, object? state) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + // The main difference between UnsafeRegister and Register appears to be that UnsafeRegister callbacks don't capture and use the execution context. + // So to emulate that here, let's suppress the execution context if needed before calling Register. + // This idea was taken from UniTask and how they implemented their RegisterWithoutCaptureExecutionContext extension methods: + // https://github.com/Cysharp/UniTask/blob/master/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenExtensions.cs + + var restoreFlow = false; + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + try + { + Action internalCallback = (data) => callback(data, target); + return target.Register(internalCallback, state, false); + } + finally + { + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + +#endif +} diff --git a/src/Polyfill/Polyfill_CancellationTokenSource.cs b/src/Polyfill/Polyfill_CancellationTokenSource.cs new file mode 100644 index 00000000..83980b66 --- /dev/null +++ b/src/Polyfill/Polyfill_CancellationTokenSource.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ +#if !NET8_0_OR_GREATER + + /// Communicates a request for cancellation asynchronously. + /// + /// + /// The associated will be notified of the cancellation + /// and will synchronously transition to a state where returns true. + /// Any callbacks or cancelable operations registered with the will be executed asynchronously, + /// with the returned representing their eventual completion. + /// + /// + /// Callbacks registered with the token should not throw exceptions. + /// However, any such exceptions that are thrown will be aggregated into an , + /// such that one callback throwing an exception will not prevent other registered callbacks from being executed. + /// + /// + /// The that was captured when each callback was registered + /// will be reestablished when the callback is invoked. + /// + /// + /// This has been disposed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancelasync")] + public static Task CancelAsync(this CancellationTokenSource target) + { + if (target.IsCancellationRequested) + { + // If cancellation has already been requested then we can just return immediately + return Task.CompletedTask; + } + else + { + // Run sync Cancel call in Task to avoid possible deadlock. + // As an example, the CancellationTokenSource_CancelAsync_CallbacksInvokedAsynchronously test + // will hit a deadlock if we try to just call Cancel directly without it being run in a task + Task task = Task.Run(() => target.Cancel()); + + while (!target.IsCancellationRequested) + { + // Don't return until we know that the cancellation request has started, to match the + // state that the real implemenation for CancelAsync would be in after being called + } + + return task; + } + } + +#endif +} diff --git a/src/Polyfill/Polyfill_Dictionary.cs b/src/Polyfill/Polyfill_Dictionary.cs new file mode 100644 index 00000000..772a8680 --- /dev/null +++ b/src/Polyfill/Polyfill_Dictionary.cs @@ -0,0 +1,35 @@ +// + +#pragma warning disable + +#if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2X + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Removes the value with the specified key from the , and copies the element + /// to the value parameter. + /// + /// A dictionary with keys of type TKey and values of type TValue. + /// The key of the element to remove. + /// The removed element. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// true if the element is successfully found and removed; otherwise, false. + /// is null. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.remove")] + public static bool Remove( + this Dictionary target, + TKey key, + [MaybeNullWhen(false)] out TValue value) + { + target.TryGetValue(key, out value); + return target.Remove(key); + } +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_HttpClient.cs b/src/Polyfill/Polyfill_HttpClient.cs new file mode 100644 index 00000000..39b940af --- /dev/null +++ b/src/Polyfill/Polyfill_HttpClient.cs @@ -0,0 +1,179 @@ +// + +#pragma warning disable + +#if ((NETFRAMEWORK && HTTPREFERENCED) || NETSTANDARD || NETCOREAPP2X || NETCOREAPP3X) + +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Send a GET request to the specified Uri and return the response body as a stream in an asynchronous operation. + /// + /// + /// This operation will not block. The returned object will complete after the response headers are read. + /// This method does not read nor buffer the response body. + /// + /// The Uri the request is sent to. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstreamasync#system-net-http-httpclient-getstreamasync(system-string-system-threading-cancellationtoken)")] + public static async Task GetStreamAsync( + this HttpClient target, + string requestUri, + CancellationToken cancellationToken = default) + { + try + { + // Must not be disposed for the stream to be usable + var response = await target.GetAsync( + requestUri, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } + // Older versions of HttpClient methods don't propagate the cancellation token inside the exception + catch (OperationCanceledException ex) when ( + ex.CancellationToken != cancellationToken && + cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(ex.Message, ex.InnerException, cancellationToken); + } + } + + /// + /// Send a GET request to the specified Uri and return the response body as a stream in an asynchronous operation. + /// + /// + /// This operation will not block. The returned object will complete after the response headers are read. + /// This method does not read nor buffer the response body. + /// + /// The Uri the request is sent to. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstreamasync#system-net-http-httpclient-getstreamasync(system-uri-system-threading-cancellationtoken)")] + public static Task GetStreamAsync( + this HttpClient target, + Uri requestUri, + CancellationToken cancellationToken = default) => + target.GetStreamAsync(requestUri.ToString(), cancellationToken); + + /// + /// Send a GET request to the specified Uri and return the response body as a byte array in an asynchronous operation. + /// + /// + /// This operation will not block. The returned Task{Byte[]} object will complete after the response headers are read. + /// This method does not read nor buffer the response body. + /// + /// The Uri the request is sent to. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync#system-net-http-httpclient-getbytearrayasync(system-string-system-threading-cancellationtoken)")] + public static async Task GetByteArrayAsync( + this HttpClient target, + string requestUri, + CancellationToken cancellationToken = default) + { + try + { + using var response = await target.GetAsync( + requestUri, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + } + // Older versions of HttpClient methods don't propagate the cancellation token inside the exception + catch (OperationCanceledException exception) when ( + exception.CancellationToken != cancellationToken && + cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(exception.Message, exception.InnerException, cancellationToken); + } + } + + /// + /// Send a GET request to the specified Uri and return the response body as a byte array in an asynchronous operation. + /// + /// + /// This operation will not block. The returned Task{byte[]} object will complete after the response headers are read. + /// This method does not read nor buffer the response body. + /// + /// The Uri the request is sent to. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getbytearrayasync#system-net-http-httpclient-getbytearrayasync(system-uri-system-threading-cancellationtoken)")] + public static Task GetByteArrayAsync( + this HttpClient target, + Uri requestUri, + CancellationToken cancellationToken = default) => + target.GetByteArrayAsync(requestUri.ToString(), cancellationToken); + + /// + /// Send a GET request to the specified Uri and return the response body as a string in an asynchronous operation. + /// + /// + /// This operation will not block. The returned object will complete after the response headers are read. + /// This method does not read nor buffer the response body. + /// + /// The Uri the request is sent to. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstringasync#system-net-http-httpclient-getstringasync(system-string-system-threading-cancellationtoken)")] + public static async Task GetStringAsync( + this HttpClient target, + string requestUri, + CancellationToken cancellationToken = default) + { + try + { + using var response = await target.GetAsync( + requestUri, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + // Older versions of HttpClient methods don't propagate the cancellation token inside the exception + catch (OperationCanceledException exception) when ( + exception.CancellationToken != cancellationToken && + cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(exception.Message, exception.InnerException, cancellationToken); + } + } + + /// + /// Send a GET request to the specified Uri and return the response body as a string in an asynchronous operation. + /// + /// + /// This operation will not block. The returned object will complete after the response headers are read. + /// This method does not read nor buffer the response body. + /// + /// The Uri the request is sent to. + /// The cancellation token to cancel the operation. + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.getstringasync#system-net-http-httpclient-getstringasync(system-uri-system-threading-cancellationtoken)")] + public static Task GetStringAsync( + this HttpClient target, + Uri requestUri, + CancellationToken cancellationToken = default) => + target.GetStringAsync(requestUri.ToString(), cancellationToken); +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_HttpContent.cs b/src/Polyfill/Polyfill_HttpContent.cs new file mode 100644 index 00000000..24715e98 --- /dev/null +++ b/src/Polyfill/Polyfill_HttpContent.cs @@ -0,0 +1,75 @@ +// + +#pragma warning disable + +#if ((NETFRAMEWORK && HTTPREFERENCED) || NETSTANDARD || NETCOREAPP2X || NETCOREAPP3X) + +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Serializes the HTTP content and returns a stream that represents the content. + /// + /// + /// Note that this method will internally buffer the content unless CreateContentReadStreamAsync() has been + /// implemented to do otherwise. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasstreamasync#system-net-http-httpcontent-readasstreamasync(system-threading-cancellationtoken)")] + public static Task ReadAsStreamAsync( + this HttpContent target, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return target.ReadAsStreamAsync(); + } + + /// + /// Serializes the HTTP content to a byte array as an asynchronous operation. + /// + /// + /// Note that this method will internally buffer the content unless CreateContentReadStreamAsync() has been + /// implemented to do otherwise. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasbytearrayasync#system-net-http-httpcontent-readasbytearrayasync(system-threading-cancellationtoken)")] + public static Task ReadAsByteArrayAsync( + this HttpContent target, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return target.ReadAsByteArrayAsync(); + } + + /// + /// Serializes the HTTP content to a string as an asynchronous operation. + /// + /// + /// Note that this method will internally buffer the content unless CreateContentReadStreamAsync() has been + /// implemented to do otherwise. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// The task object representing the asynchronous operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.readasstringasync#system-net-http-httpcontent-readasstringasync(system-threading-cancellationtoken)")] + public static Task ReadAsStringAsync( + this HttpContent target, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return target.ReadAsStringAsync(); + } +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_IEnumerable.cs b/src/Polyfill/Polyfill_IEnumerable.cs new file mode 100644 index 00000000..f5e211ac --- /dev/null +++ b/src/Polyfill/Polyfill_IEnumerable.cs @@ -0,0 +1,306 @@ +// + +#pragma warning disable + +using System; +using System.Collections.Generic; +using Link = System.ComponentModel.DescriptionAttribute; +using System.Linq; + +static partial class Polyfill +{ + /// + /// Produces a set items excluding by using the default equality comparer to compare values. + /// + /// An whose elements that are not equal to will be returned. + /// An that is elements equal it will cause those elements to be removed from the returned sequence. + /// The type of the elements of . + /// A sequence that contains the items of but excluding . + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except?view=net-8.0#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0)))")] + public static IEnumerable Except( + this IEnumerable target, + TSource item) => + Except(target, item, null); + + /// + /// Produces the set difference of two sequences by using the default equality comparer to compare values. + /// + /// An whose elements that are not equal to will be returned. + /// An that is elements equal it will cause those elements to be removed from the returned sequence. + /// The type of the elements of . + /// A sequence that contains the items of but excluding . + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except?view=net-8.0#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0)))")] + public static IEnumerable Except( + this IEnumerable target, + params TSource[] items) => + target.Except((IEnumerable)items); + + /// + /// Produces a set items excluding by using to compare values. + /// + /// An whose elements that are not equal to will be returned. + /// An that is elements equal it will cause those elements to be removed from the returned sequence. + /// An to compare values. + /// The type of the elements of . + /// A sequence that contains the items of but excluding . + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))")] + public static IEnumerable Except( + this IEnumerable target, + TSource item, + IEqualityComparer? comparer) + { + var set = new HashSet(comparer); + set.Add(item); + foreach (TSource element in target) + { + if (set.Add(element)) + { + yield return element; + } + } + } + + /// + /// Produces the set difference of two sequences by to compare values. + /// + /// An whose elements that are not equal to will be returned. + /// An that is elements equal it will cause those elements to be removed from the returned sequence. + /// The type of the elements of . + /// A sequence that contains the items of but excluding . + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.except#system-linq-enumerable-except-1(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))")] + public static IEnumerable Except( + this IEnumerable target, + IEqualityComparer comparer, + params TSource[] items) => + target.Except((IEnumerable)items, comparer); + + +#if NETSTANDARD || NETCOREAPPX || NETFRAMEWORK || NET5_0 + + /// + /// Split the elements of a sequence into chunks of size at most . + /// + /// + /// Every chunk except the last will be of size . + /// The last chunk will contain the remaining elements and may be of a smaller size. + /// + /// An whose elements to chunk. + /// Maximum size of each chunk. + /// The type of the elements of source. + /// + /// An that contains the elements the input sequence split into chunks of size . + /// + /// is null. + /// is below 1. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk")] + public static IEnumerable Chunk(this IEnumerable source, int size) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (size < 1) + { + throw new ArgumentOutOfRangeException(nameof(size), size, "Size must be greater than 0."); + } + + return ChunkIterator(source, size); + + static IEnumerable ChunkIterator(IEnumerable source, int size) + { + using IEnumerator e = source.GetEnumerator(); + + // Before allocating anything, make sure there's at least one element. + if (e.MoveNext()) + { + // Now that we know we have at least one item, allocate an initial storage array. This is not + // the array we'll yield. It starts out small in order to avoid significantly overallocating + // when the source has many fewer elements than the chunk size. + int arraySize = Math.Min(size, 4); + int i; + do + { + var array = new TSource[arraySize]; + + // Store the first item. + array[0] = e.Current; + i = 1; + + if (size != array.Length) + { + // This is the first chunk. As we fill the array, grow it as needed. + for (; i < size && e.MoveNext(); i++) + { + if (i >= array.Length) + { + arraySize = (int)Math.Min((uint)size, 2 * (uint)array.Length); + Array.Resize(ref array, arraySize); + } + + array[i] = e.Current; + } + } + else + { + // For all but the first chunk, the array will already be correctly sized. + // We can just store into it until either it's full or MoveNext returns false. + // avoid bounds checks by using cached local (`array` is lifted to iterator object as a field) + TSource[] local = array; + for (; (uint)i < (uint)local.Length && e.MoveNext(); i++) + { + local[i] = e.Current; + } + } + + if (i != array.Length) + { + Array.Resize(ref array, i); + } + + yield return array; + } + while (i >= size && e.MoveNext()); + } + } + } + + /// + /// Returns the maximum value in a generic sequence according to a specified key selector function. + /// + /// The type of the elements of . + /// The type of key to compare elements by. + /// A sequence of values to determine the maximum value of. + /// A function to extract the key for each element. + /// The value with the maximum key in the sequence. + /// is . + /// No key extracted from implements the or interface. + /// + /// If is a reference type and the source sequence is empty or contains only values that are , this method returns . + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))")] + public static TSource? MaxBy( + this IEnumerable target, + Func keySelector) => + MaxBy(target, keySelector, null); + + /// Returns the maximum value in a generic sequence according to a specified key selector function. + /// The type of the elements of . + /// The type of key to compare elements by. + /// A sequence of values to determine the maximum value of. + /// A function to extract the key for each element. + /// The to compare keys. + /// The value with the maximum key in the sequence. + /// is . + /// No key extracted from implements the or interface. + /// + /// If is a reference type and the source sequence is empty or contains only values that are , this method returns . + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0#system-linq-enumerable-maxby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))")] + public static TSource? MaxBy( + this IEnumerable target, + Func keySelector, + IComparer? comparer) => + target + .OrderByDescending(keySelector, comparer) + .FirstOrDefault(); + + /// + /// Returns the minimum value in a generic sequence according to a specified key selector function. + /// + /// The type of the elements of . + /// The type of key to compare elements by. + /// A sequence of values to determine the minby value of. + /// A function to extract the key for each element. + /// The value with the minimum key in the sequence. + /// is . + /// No key extracted from implements the or interface. + /// + /// If is a reference type and the source sequence is empty or contains only values that are , this method returns . + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1)))")] + public static TSource? MinBy( + this IEnumerable target, + Func keySelector) => + MinBy(target, keySelector, null); + + /// Returns the minimum value in a generic sequence according to a specified key selector function. + /// The type of the elements of . + /// The type of key to compare elements by. + /// A sequence of values to determine the minimum value of. + /// A function to extract the key for each element. + /// The to compare keys. + /// The value with the minimum key in the sequence. + /// is . + /// No key extracted from implements the or interface. + /// + /// If is a reference type and the source sequence is empty or contains only values that are , this method returns . + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.minby?view=net-8.0#system-linq-enumerable-minby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-icomparer((-1)))")] + public static TSource? MinBy( + this IEnumerable target, + Func keySelector, + IComparer? comparer) => + target + .OrderBy(keySelector, comparer) + .FirstOrDefault(); + +#endif + +#if NET46X || NET47 + + /// + /// Appends a value to the end of the sequence. + /// + /// A sequence of values. + /// The value to append to . + /// The type of the elements of source. + /// A new sequence that ends with element. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.append")] + public static IEnumerable Append( + this IEnumerable target, + TSource element) + { + foreach (var item in target) + { + yield return item; + } + + yield return element; + } +#endif + +#if NETFRAMEWORK || NETSTANDARD2_0 + /// + /// Returns a new enumerable collection that contains the elements from source with the last count elements of the + /// source collection omitted. + /// + /// An enumerable collection instance. + /// The number of elements to omit from the end of the collection. + /// The type of the elements in the enumerable collection. + /// A new enumerable collection that contains the elements from source minus count elements from the end + /// of the collection. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast")] + public static IEnumerable SkipLast( + this IEnumerable target, + int count) => + target.Reverse().Skip(count).Reverse(); +#endif + +#if NET471 || NET46X || NETSTANDARD2_0 + + /// + /// Creates a HashSet from an IEnumerable using the comparer to compare keys. + /// + /// An IEnumerable to create a HashSet from. + /// An IEqualityComparer to compare keys. + /// The type of the elements of source. + /// A HashSet that contains values of type TSource selected from the input sequence. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tohashset#system-linq-enumerable-tohashset-1(system-collections-generic-ienumerable((-0))-system-collections-generic-iequalitycomparer((-0)))")] + public static HashSet ToHashSet( + this IEnumerable target, + IEqualityComparer? comparer = null) => + new HashSet(target, comparer); + +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_IReadOnlyDictionary.cs b/src/Polyfill/Polyfill_IReadOnlyDictionary.cs new file mode 100644 index 00000000..fc6d8e16 --- /dev/null +++ b/src/Polyfill/Polyfill_IReadOnlyDictionary.cs @@ -0,0 +1,63 @@ +// + +#pragma warning disable + +#if NETFRAMEWORK || NETSTANDARD2_0 + +using System; +using System.Collections.Generic; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Tries to get the value associated with the specified key in the dictionary. + /// + /// A dictionary with keys of type TKey and values of type TValue. + /// The key of the value to get. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// + /// A TValue instance. When the method is successful, the returned object is the value associated with + /// the specified key. When the method fails, it returns the default value for TValue. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault")] + public static TValue? GetValueOrDefault( + this IReadOnlyDictionary target, + TKey key) + { + if (target.TryGetValue(key, out var result)) + { + return result; + } + + return default; + } + + /// + /// Tries to get the value associated with the specified key in the dictionary. + /// + /// A dictionary with keys of type TKey and values of type TValue. + /// The key of the value to get. + /// A dictionary with keys of type TKey and values of type TValue. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// + /// A TValue instance. When the method is successful, the returned object is the value associated with + /// the specified key. When the method fails, it returns the default value for TValue. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault#system-collections-generic-collectionextensions-getvalueordefault-2(system-collections-generic-ireadonlydictionary((-0-1))-0-1)")] + public static TValue GetValueOrDefault( + this IReadOnlyDictionary target, + TKey key, + TValue defaultValue = default) + { + if (target.TryGetValue(key, out var result)) + { + return result!; + } + + return defaultValue; + } +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_KeyValuePair.cs b/src/Polyfill/Polyfill_KeyValuePair.cs new file mode 100644 index 00000000..cc218aa1 --- /dev/null +++ b/src/Polyfill/Polyfill_KeyValuePair.cs @@ -0,0 +1,28 @@ +// + +#pragma warning disable + +#if NETFRAMEWORK || NETSTANDARD2_0 + +using System; +using System.Collections.Generic; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Deconstructs the current + /// + /// The key of the current . + /// The value of the current . + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.keyvaluepair-2.deconstruct")] + public static void Deconstruct( + this KeyValuePair target, + out TKey key, + out TValue value) + { + key = target.Key; + value = target.Value; + } +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_Memory.cs b/src/Polyfill/Polyfill_Memory.cs new file mode 100644 index 00000000..827c1bee --- /dev/null +++ b/src/Polyfill/Polyfill_Memory.cs @@ -0,0 +1,136 @@ +// + +#pragma warning disable + +#if MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X) + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Indicates whether a specified value is found in a read-only span. Values are compared using IEquatable{T}.Equals(T). + /// + /// The value to search for. + /// true if found, false otherwise. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.contains#system-memoryextensions-contains-1(system-readonlyspan((-0))-0)")] + public static bool Contains( + this ReadOnlySpan target, + T value) + where T : IEquatable + { + for (var index = 0; index < target.Length; index++) + { + if (target[index].Equals(value)) + { + return true; + } + } + + return false; + } + + /// + /// Indicates whether a specified value is found in a only span. Values are compared using IEquatable{T}.Equals(T). + /// + /// The value to search for. + /// true if found, false otherwise. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.contains#system-memoryextensions-contains-1(system-span((-0))-0)")] + public static bool Contains( + this Span target, + T value) + where T : IEquatable + { + for (var index = 0; index < target.Length; index++) + { + if (target[index].Equals(value)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether two sequences are equal by comparing the elements using IEquatable{T}.Equals(T). + /// + /// The first sequence to compare. + /// The second sequence to compare. + /// true if the two sequences are equal; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-readonlyspan((-0))-system-readonlyspan((-0)))")] + public static bool SequenceEqual( + this ReadOnlySpan target, + string other) => + target.SequenceEqual(other.AsSpan()); + + /// + /// Determines whether two sequences are equal by comparing the elements using IEquatable{T}.Equals(T). + /// + /// The first sequence to compare. + /// The second sequence to compare. + /// true if the two sequences are equal; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-span((-0))-system-readonlyspan((-0)))")] + public static bool SequenceEqual( + this Span target, + string other) => + target.SequenceEqual(other.AsSpan()); + + /// + /// Determines whether a read-only character span begins with a specified value when compared using a specified value. + /// + /// The source span. + /// The sequence to compare to the beginning of the source span. + /// An enumeration value that determines how span and value are compared. + /// true if value matches the beginning of span; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith#system-memoryextensions-startswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))")] + public static bool StartsWith( + this ReadOnlySpan target, + string other, + StringComparison comparison = StringComparison.CurrentCulture) => + target.StartsWith(other.AsSpan(), comparison); + + /// + /// Determines whether a specified sequence appears at the start of a span. + /// + /// The source span. + /// The sequence to compare to the beginning of the source span. + /// true if value matches the beginning of span; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith#system-memoryextensions-startswith-1(system-span((-0))-system-readonlyspan((-0)))")] + public static bool StartsWith( + this Span target, + string other) => + target.StartsWith(other.AsSpan()); + + /// + /// Determines whether the end of the span matches the specified value when compared using the specified option. + /// + /// The source span. + /// The sequence to compare to the end of the source span. + /// An enumeration value that determines how span and value are compared. + /// true if value matches the end of span; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.endswith#system-memoryextensions-endswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))")] + public static bool EndsWith( + this ReadOnlySpan target, + string other, + StringComparison comparison = StringComparison.CurrentCulture) => + target.EndsWith(other.AsSpan(), comparison); + + /// + /// Determines whether the specified sequence appears at the end of a span. + /// + /// The source span. + /// The sequence to compare to the end of the source span. + /// true if value matches the end of span; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.endswith#system-memoryextensions-endswith-1(system-span((-0))-system-readonlyspan((-0)))")] + public static bool EndsWith( + this Span target, + string other) => + target.EndsWith(other.AsSpan()); +} + +#endif diff --git a/src/Polyfill/Polyfill_MicroNanosecond.cs b/src/Polyfill/Polyfill_MicroNanosecond.cs new file mode 100644 index 00000000..90435ede --- /dev/null +++ b/src/Polyfill/Polyfill_MicroNanosecond.cs @@ -0,0 +1,121 @@ +// + +#pragma warning disable + +using System; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + +#if NET7_0_OR_GREATER + /// + /// Gets the nanosecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.timespan.nanoseconds")] + public static int Nanoseconds(this TimeSpan target) => + target.Nanoseconds; + + /// + /// Gets the nanosecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetime.nanosecond")] + public static int Nanosecond(this DateTime target) => + target.Nanosecond; + + /// + /// Gets the nanosecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.nanosecond")] + public static int Nanosecond(this DateTimeOffset target) => + target.Nanosecond; + + /// + /// Gets the microsecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.timespan.microsecond")] + public static int Microseconds(this TimeSpan target) => + target.Microseconds; + + /// + /// Gets the microsecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetime.microsecond")] + public static int Microsecond(this DateTime target) => + target.Microsecond; + + /// + /// Gets the microsecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.microsecond")] + public static int Microsecond(this DateTimeOffset target) => + target.Microsecond; + +#else + + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + + /// + /// Gets the nanosecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.timespan.nanoseconds")] + public static int Nanoseconds(this TimeSpan target) => + (int) (target.TicksComponent() % TicksPerMicrosecond) * 100; + + /// + /// Gets the nanosecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetime.nanosecond")] + public static int Nanosecond(this DateTime target) => + (int) (target.TicksComponent() % TicksPerMicrosecond) * 100; + + /// + /// Gets the nanosecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.nanosecond")] + public static int Nanosecond(this DateTimeOffset target) => + (int) (target.TicksComponent() % TicksPerMicrosecond) * 100; + + /// + /// Gets the microsecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.timespan.microseconds")] + public static int Microseconds(this TimeSpan target) => + (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + + /// + /// Gets the microsecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetime.microsecond")] + public static int Microsecond(this DateTime target) => + (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + + /// + /// Gets the microsecond component of the time represented by the current object. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.microsecond")] + public static int Microsecond(this DateTimeOffset target) => + (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + + static long TicksComponent(this TimeSpan target) + { + var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); + var secondsPart = target - noSeconds; + return secondsPart.Ticks; + } + + static long TicksComponent(this DateTime target) + { + var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); + var secondsPart = target - noSeconds; + return secondsPart.Ticks; + } + + static long TicksComponent(this DateTimeOffset target) + { + var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); + var secondsPart = target - noSeconds; + return secondsPart.Ticks; + } +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_MicroNanosecondAdd.cs b/src/Polyfill/Polyfill_MicroNanosecondAdd.cs new file mode 100644 index 00000000..43c5e56f --- /dev/null +++ b/src/Polyfill/Polyfill_MicroNanosecondAdd.cs @@ -0,0 +1,28 @@ +// + +#pragma warning disable + +using System; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + +#if !NET7_0_OR_GREATER + + /// + /// Returns a new object that adds a specified number of microseconds to the value of this instance.. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetime.addmicroseconds")] + public static DateTime AddMicroseconds(this DateTime target, double microseconds) => + target.AddMilliseconds(microseconds / 1000); + + /// + /// Returns a new object that adds a specified number of microseconds to the value of this instance.. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.addmicroseconds")] + public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => + target.AddMilliseconds(microseconds / 1000); + +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_Process.cs b/src/Polyfill/Polyfill_Process.cs new file mode 100644 index 00000000..76d3fb70 --- /dev/null +++ b/src/Polyfill/Polyfill_Process.cs @@ -0,0 +1,117 @@ +// + +#pragma warning disable + +#if !NET5_0_OR_GREATER + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Instructs the Process component to wait for the associated process to exit, or + /// for the to be canceled. + /// + /// + /// Calling this method will set to . + /// + /// + /// A task that will complete when the process has exited, cancellation has been requested, + /// or an error occurs. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync")] + public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) + { + // Because the process has already started by the time this method is called, + // we're in a race against the process to set up our exit handlers before the process + // exits. As a result, there are several different flows that must be handled: + // + // CASE 1: WE ENABLE EVENTS + // This is the "happy path". In this case we enable events. + // + // CASE 1.1: PROCESS EXITS OR IS CANCELED AFTER REGISTERING HANDLER + // This case continues the "happy path". The process exits or waiting is canceled after + // registering the handler and no special cases are needed. + // + // CASE 1.2: PROCESS EXITS BEFORE REGISTERING HANDLER + // It's possible that the process can exit after we enable events but before we register + // the handler. In that case we must check for exit after registering the handler. + // + // + // CASE 2: PROCESS EXITS BEFORE ENABLING EVENTS + // The process may exit before we attempt to enable events. In that case EnableRaisingEvents + // will throw an exception like this: + // System.InvalidOperationException : Cannot process request because the process (42) has exited. + // In this case we catch the InvalidOperationException. If the process has exited, our work + // is done and we return. If for any reason (now or in the future) enabling events fails + // and the process has not exited, bubble the exception up to the user. + // + // + // CASE 3: USER ALREADY ENABLED EVENTS + // In this case the user has already enabled raising events. Re-enabling events is a no-op + // as the value hasn't changed. However, no-op also means that if the process has already + // exited, EnableRaisingEvents won't throw an exception. + // + // CASE 3.1: PROCESS EXITS OR IS CANCELED AFTER REGISTERING HANDLER + // (See CASE 1.1) + // + // CASE 3.2: PROCESS EXITS BEFORE REGISTERING HANDLER + // (See CASE 1.2) + + if (!target.HasExited) + { + // Early out for cancellation before doing more expensive work + cancellationToken.ThrowIfCancellationRequested(); + } + + try + { + // CASE 1: We enable events + // CASE 2: Process exits before enabling events (and throws an exception) + // CASE 3: User already enabled events (no-op) + target.EnableRaisingEvents = true; + } + catch (InvalidOperationException) + { + // CASE 2: If the process has exited, our work is done, otherwise bubble the + // exception up to the user + if (target.HasExited) + { + return; + } + + throw; + } + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + EventHandler handler = (_, _) => tcs.TrySetResult(null); + target.Exited += handler; + + try + { + if (target.HasExited) + { + // CASE 1.2 & CASE 3.2: Handle race where the process exits before registering the handler + } + else + { + // CASE 1.1 & CASE 3.1: Process exits or is canceled here + using (cancellationToken.UnsafeRegister(static (s, cancellationToken) => ((TaskCompletionSource)s!).TrySetCanceled(cancellationToken), tcs)) + { + await tcs.Task.ConfigureAwait(false); + } + } + } + finally + { + target.Exited -= handler; + } + } +} + +#endif diff --git a/src/Polyfill/Polyfill_Stream.cs b/src/Polyfill/Polyfill_Stream.cs new file mode 100644 index 00000000..b1e875f6 --- /dev/null +++ b/src/Polyfill/Polyfill_Stream.cs @@ -0,0 +1,87 @@ +// + +#pragma warning disable + +#if TASKSEXTENSIONSREFERENCED && (NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0) + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ +#if MEMORYREFERENCED + + /// + /// Asynchronously reads a sequence of bytes from the current stream, advances the position within the stream by + /// the number of bytes read, and monitors cancellation requests. + /// + /// The region of memory to write the data into. + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// + /// A task that represents the asynchronous read operation. The value of its Result property contains the + /// total number of bytes read into the buffer. The result value can be less than the number of bytes allocated in + /// the buffer if that many bytes are not currently available, or it can be 0 (zero) if the end of the stream has + /// been reached. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.readasync#system-io-stream-readasync(system-memory((system-byte))-system-threading-cancellationtoken)")] + public static ValueTask ReadAsync( + this Stream target, + Memory buffer, + CancellationToken cancellationToken = default) + { + if (!MemoryMarshal.TryGetArray((ReadOnlyMemory) buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position + /// within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// The region of memory to write data from. + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// A task that represents the asynchronous write operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.writeasync#system-io-stream-writeasync(system-readonlymemory((system-byte))-system-threading-cancellationtoken)")] + public static ValueTask WriteAsync( + this Stream target, + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) + { + if (!MemoryMarshal.TryGetArray(buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + +#endif + + /// + /// Asynchronously reads the bytes from the current stream and writes them to another stream, using a specified + /// cancellation token. Both streams positions are advanced by the number of bytes copied. + /// + /// The stream to which the contents of the current stream will be copied. + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// A task that represents the asynchronous copy operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.copytoasync#system-io-stream-copytoasync(system-io-stream-system-threading-cancellationtoken)")] + public static Task CopyToAsync( + this Stream target, + Stream destination, + CancellationToken cancellationToken = default) => + target.CopyToAsync(destination, 81920, cancellationToken); +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_String.cs b/src/Polyfill/Polyfill_String.cs new file mode 100644 index 00000000..e71ae9e1 --- /dev/null +++ b/src/Polyfill/Polyfill_String.cs @@ -0,0 +1,139 @@ +// + +#pragma warning disable + +using System; +using Link = System.ComponentModel.DescriptionAttribute; +using System.Text; + +static partial class Polyfill +{ +#if HAS_SPAN && !NET6_0_OR_GREATER + + /// + /// Copies the contents of this string into the destination span. + /// + /// The span into which to copy this string's contents + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.copyto")] + public static void CopyTo(this string target, Span destination) => + target.AsSpan().CopyTo(destination); + + /// + /// Copies the contents of this string into the destination span. + /// + /// The span into which to copy this string's contents + /// true if the data was copied; false if the destination was too short to fit the contents of the string. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.trycopyto")] + public static bool TryCopyTo(this string target, Span destination) => + target.AsSpan().TryCopyTo(destination); +#endif + +#if NETFRAMEWORK || NETSTANDARD2_0 + + /// + /// Returns the hash code for this string using the specified rules. + /// + /// One of the enumeration values that specifies the rules to use in the comparison. + /// A 32-bit signed integer hash code. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.gethashcode#system-string-gethashcode(system-stringcomparison)")] + public static int GetHashCode(this string target, StringComparison comparisonType) => + FromComparison(comparisonType).GetHashCode(target); + + static StringComparer FromComparison(StringComparison comparison) => + comparison switch + { + StringComparison.CurrentCulture => StringComparer.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, + StringComparison.Ordinal => StringComparer.Ordinal, + StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null) + }; + + /// + /// Returns a value indicating whether a specified string occurs within this string, using the specified comparison rules. + /// + /// The string to seek. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// true if the value parameter occurs within this string, or if value is the empty string (""); otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-string-system-stringcomparison)")] + public static bool Contains(this string target, string value, StringComparison comparisonType) => + target.IndexOf(value, comparisonType) >= 0; + + /// + /// Determines whether this string instance starts with the specified character. + /// + /// The character to compare. + /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison. + /// true if value matches the beginning of this string; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)")] + public static bool StartsWith(this string target, char value) + { + if (target.Length == 0) + { + return false; + } + + return target[0] == value; + } + + /// + /// Returns a value indicating whether a specified character occurs within this string. + /// + /// The character to seek. + /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison. + /// true if the value parameter occurs within this string; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)")] + public static bool EndsWith(this string target, char value) + { + if (target.Length == 0) + { + return false; + } + + var lastPos = target.Length - 1; + return lastPos < target.Length && + target[lastPos] == value; + } + + /// + /// Splits a string into a maximum number of substrings based on a specified delimiting character and, optionally, + /// options. Splits a string into a maximum number of substrings based on the provided character separator, + /// optionally omitting empty substrings from the result. + /// + /// A character that delimits the substrings in this instance. + /// A bitwise combination of the enumeration values that specifies whether to trim substrings + /// and include empty substrings. + /// An array that contains at most count substrings from this instance that are delimited by separator. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.split#system-string-split(system-char-system-stringsplitoptions)")] + public static string[] Split(this string target, char separator, StringSplitOptions options = StringSplitOptions.None) => + target.Split([separator], options); + + /// + /// Splits a string into a maximum number of substrings based on a specified delimiting character and, optionally, + /// options. Splits a string into a maximum number of substrings based on the provided character separator, + /// optionally omitting empty substrings from the result. + /// + /// A character that delimits the substrings in this instance. + /// The maximum number of elements expected in the array. + /// A bitwise combination of the enumeration values that specifies whether to trim substrings + /// and include empty substrings. + /// An array that contains at most count substrings from this instance that are delimited by separator. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.split#system-string-split(system-char-system-int32-system-stringsplitoptions)")] + public static string[] Split(this string target, char separator, int count, StringSplitOptions options = StringSplitOptions.None) => + target.Split([separator], count, options); +#endif + +#if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0 + /// + /// Returns a value indicating whether a specified character occurs within this string. + /// + /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison. + /// The character to seek. + /// true if the value parameter occurs within this string; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.string.contains#system-string-contains(system-char)")] + public static bool Contains(this string target, char value) => + target.IndexOf(value) >= 0; +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_StringBuilder.cs b/src/Polyfill/Polyfill_StringBuilder.cs new file mode 100644 index 00000000..8863614f --- /dev/null +++ b/src/Polyfill/Polyfill_StringBuilder.cs @@ -0,0 +1,259 @@ +// + +#pragma warning disable + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ +#if HAS_SPAN && (!NETSTANDARD2_1_OR_GREATER && !NETCOREAPP2_1_OR_GREATER) + + /// + /// Copies the characters from a specified segment of this instance to a destination Char span. + /// + /// The starting position in this instance where characters will be copied from. The index is zero-based. + /// The writable span where characters will be copied. + /// The number of characters to be copied. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.copyto#system-text-stringbuilder-copyto(system-int32-system-span((system-char))-system-int32)")] + public static void CopyTo( + this StringBuilder target, + int sourceIndex, + Span destination, + int count) + { + var destinationIndex = 0; + while (true) + { + if (sourceIndex == target.Length) + { + break; + } + + if (destinationIndex == count) + { + break; + } + + destination[destinationIndex] = target[sourceIndex]; + destinationIndex++; + sourceIndex++; + } + } + + /// + /// Appends the string representation of a specified read-only character span to this instance. + /// + /// The read-only character span to append. + /// A reference to this instance after the append operation is completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-readonlyspan((system-char)))")] + public static StringBuilder Append(this StringBuilder target, ReadOnlySpan value) + { + if (value.Length <= 0) + { + return target; + } + +#if AllowUnsafeBlocks + unsafe + { + fixed (char* valueChars = value) + { + target.Append(valueChars, value.Length); + } + } +#else + target.Append(value.ToString()); +#endif + return target; + } + + /// + /// Returns a value indicating whether the characters in this instance are equal to the characters in a specified + /// read-only character span. + /// + /// The character span to compare with the current instance. + /// + /// The Equals method performs an ordinal comparison to determine whether the characters in the current instance + /// and span are equal. + /// + /// true if the characters in this instance and span are the same; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.equals#system-text-stringbuilder-equals(system-readonlyspan((system-char)))")] + public static bool Equals(this StringBuilder target, ReadOnlySpan span) + { + if (target.Length != span.Length) + { + return false; + } + + for (var index = 0; index < target.Length; index++) + { + var ch1 = target[index]; + var ch2 = span[index]; + if (ch1 != ch2) + { + return false; + } + } + + return true; + } + +#endif + +#if HAS_SPAN && !NET6_0_OR_GREATER + /// Appends the specified interpolated string to this instance. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder Append( + StringBuilder target, + [InterpolatedStringHandlerArgument(nameof(target))] ref AppendInterpolatedStringHandler handler) => target; + + /// Appends the specified interpolated string to this instance. + /// An object that supplies culture-specific formatting information. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder Append( + StringBuilder target, + IFormatProvider? provider, + [InterpolatedStringHandlerArgument(nameof(target), nameof(provider))] ref AppendInterpolatedStringHandler handler) => target; + + /// Appends the specified interpolated string followed by the default line terminator to the end of the current StringBuilder object. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder AppendLine( + StringBuilder target, + [InterpolatedStringHandlerArgument(nameof(target))] ref AppendInterpolatedStringHandler handler) => + target.AppendLine(); + + /// Appends the specified interpolated string followed by the default line terminator to the end of the current StringBuilder object. + /// An object that supplies culture-specific formatting information. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder AppendLine( + StringBuilder target, + IFormatProvider? provider, + [InterpolatedStringHandlerArgument(nameof(target), nameof(provider))] ref AppendInterpolatedStringHandler handler) => + target.AppendLine(); + +#elif NET6_0_OR_GREATER + + /// Appends the specified interpolated string to this instance. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder Append( + StringBuilder target, + [InterpolatedStringHandlerArgument(nameof(target))] ref StringBuilder.AppendInterpolatedStringHandler handler) => + target.Append(ref handler); + + /// Appends the specified interpolated string to this instance. + /// An object that supplies culture-specific formatting information. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append#system-text-stringbuilder-append(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder Append( + StringBuilder target, + IFormatProvider? provider, + [InterpolatedStringHandlerArgument(nameof(target), nameof(provider))] ref StringBuilder.AppendInterpolatedStringHandler handler) => + target.Append(provider, ref handler); + + /// Appends the specified interpolated string followed by the default line terminator to the end of the current StringBuilder object. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder AppendLine( + StringBuilder target, + [InterpolatedStringHandlerArgument(nameof(target))] ref StringBuilder.AppendInterpolatedStringHandler handler) => + target.AppendLine(ref handler); + + /// Appends the specified interpolated string followed by the default line terminator to the end of the current StringBuilder object. + /// An object that supplies culture-specific formatting information. + /// The interpolated string to append. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendline#system-text-stringbuilder-appendline(system-iformatprovider-system-text-stringbuilder-appendinterpolatedstringhandler@)")] + public static StringBuilder AppendLine( + StringBuilder target, + IFormatProvider? provider, + [InterpolatedStringHandlerArgument(nameof(target), nameof(provider))] ref StringBuilder.AppendInterpolatedStringHandler handler) => + target.AppendLine(provider, ref handler); +#endif + + #if NETSTANDARD2_0|| NETFRAMEWORK + + /// Concatenates the strings of the provided array, using the specified separator between each string, then appends the result to the current instance of the string builder. + /// The string to use as a separator. separator is included in the joined strings only if values has more than one element. + /// An array that contains the strings to concatenate and append to the current instance of the string builder. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-string-system-string())")] + public static StringBuilder AppendJoin( + this StringBuilder target, + string separator, + params string[] values) => + target.Append(string.Join(separator, values)); + + /// Concatenates the string representations of the elements in the provided array of objects, using the specified separator between each member, then appends the result to the current instance of the string builder. + /// The string to use as a separator. separator is included in the joined strings only if values has more than one element. + /// An array that contains the strings to concatenate and append to the current instance of the string builder. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-string-system-object())")] + public static StringBuilder AppendJoin( + this StringBuilder target, + string separator, + params Object[] values) => + target.Append(string.Join(separator, values)); + + /// Concatenates the strings of the provided array, using the specified char separator between each string, then appends the result to the current instance of the string builder. + /// The character to use as a separator. separator is included in the joined strings only if values has more than one element. + /// An array that contains the strings to concatenate and append to the current instance of the string builder. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-char-system-string())")] + public static StringBuilder AppendJoin( + this StringBuilder target, + char separator, + params string[] values) => + target.Append(string.Join(separator.ToString(), values)); + + /// Concatenates the string representations of the elements in the provided array of objects, using the specified char separator between each member, then appends the result to the current instance of the string builder. + /// The character to use as a separator. separator is included in the joined strings only if values has more than one element. + /// An array that contains the strings to concatenate and append to the current instance of the string builder. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin(system-char-system-object())")] + public static StringBuilder AppendJoin( + this StringBuilder target, + char separator, + params object[] values) => + target.Append(string.Join(separator.ToString(), values)); + + /// Concatenates and appends the members of a collection, using the specified char separator between each member. + /// The character to use as a separator. separator is included in the joined strings only if values has more than one element. + /// A collection that contains the objects to concatenate and append to the current instance of the string builder. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin-1(system-char-system-collections-generic-ienumerable((-0)))")] + public static StringBuilder AppendJoin( + this StringBuilder target, + char separator, + params T[] values) => + target.Append(string.Join(separator.ToString(), values)); + + /// Concatenates and appends the members of a collection, using the specified char separator between each member. + /// The string to use as a separator. separator is included in the concatenated and appended strings only if values has more than one element. + /// A collection that contains the objects to concatenate and append to the current instance of the string builder. + /// A reference to this instance after the append operation has completed. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendjoin?view=netcore-2.0#system-text-stringbuilder-appendjoin-1(system-string-system-collections-generic-ienumerable((-0)))")] + public static StringBuilder AppendJoin( + this StringBuilder target, + string separator, + params T[] values) => + target.Append(string.Join(separator.ToString(), values)); + +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_Task.cs b/src/Polyfill/Polyfill_Task.cs new file mode 100644 index 00000000..7453a3d0 --- /dev/null +++ b/src/Polyfill/Polyfill_Task.cs @@ -0,0 +1,128 @@ +// + +#pragma warning disable + +#if NETFRAMEWORK || NETSTANDARD || NETCOREAPPX || NET5_0 + +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + // Copied from .NET library const Timer.MaxSupportedTimeout + private const uint MaxSupportedTimeout = 0xfffffffe; + + /// Gets a that will complete when this completes or when the specified has cancellation requested. + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-threading-cancellationtoken)")] + public static Task WaitAsync(this Task target, CancellationToken cancellationToken) => + target.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); + + /// Gets a that will complete when this completes or when the specified timeout expires. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-timespan)")] + public static Task WaitAsync( + this Task target, + TimeSpan timeout) => + target.WaitAsync(timeout, default); + + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-timespan-system-threading-cancellationtoken)")] + public static Task WaitAsync( + this Task target, + TimeSpan timeout, + CancellationToken cancellationToken) + { + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } + + long totalMilliseconds = (long) timeout.TotalMilliseconds; + if (totalMilliseconds < -1 || totalMilliseconds > MaxSupportedTimeout) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + + if (target.IsCompleted || (!cancellationToken.CanBeCanceled && timeout == Timeout.InfiniteTimeSpan)) + { + return target; + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (timeout == TimeSpan.Zero) + { + return Task.FromException(new TimeoutException()); + } + + var delayTask = Task.Delay(timeout, cancellationToken); + + Func, Task> continueFunc = completedTask => + { + if (cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + else if (completedTask.Result == delayTask) + { + throw new TimeoutException($"Execution did not complete within the time allotted {timeout.TotalMilliseconds} ms"); + } + + return target; + }; + + return Task.WhenAny(target, delayTask).ContinueWith(continueFunc, TaskContinuationOptions.OnlyOnRanToCompletion); + } + + /// + /// Gets a that will complete when this completes, or when the specified has cancellation requested. + /// + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync#system-threading-tasks-task-waitasync(system-threading-cancellationtoken)")] + public static Task WaitAsync( + this Task target, + CancellationToken cancellationToken) => + target.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); + + /// + /// Gets a that will complete when this completes, or when the specified timeout expires. + /// + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.waitasync#system-threading-tasks-task-1-waitasync(system-timespan)")] + public static Task WaitAsync( + this Task target, + TimeSpan timeout) => + target.WaitAsync(timeout, default); + + /// + /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested. + /// + /// The timeout after which the should be faulted with a if it hasn't otherwise completed. + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.waitasync#system-threading-tasks-task-1-waitasync(system-timespan-system-threading-cancellationtoken)")] + public static async Task WaitAsync( + this Task target, + TimeSpan timeout, + CancellationToken cancellationToken) + { + await ((Task) target).WaitAsync(timeout, cancellationToken); + return target.Result; + } +} + +#endif diff --git a/src/Polyfill/Polyfill_TextReader.cs b/src/Polyfill/Polyfill_TextReader.cs new file mode 100644 index 00000000..f642ea49 --- /dev/null +++ b/src/Polyfill/Polyfill_TextReader.cs @@ -0,0 +1,87 @@ +// + +#pragma warning disable + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Link = System.ComponentModel.DescriptionAttribute; +using System.Threading; +using System.Threading.Tasks; + +static partial class Polyfill +{ +#if TASKSEXTENSIONSREFERENCED && MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0) + /// + /// Asynchronously reads the characters from the current stream into a memory block. + /// + /// + /// When this method returns, contains the specified memory block of characters replaced by the characters read + /// from the current source. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// + /// + /// A value task that represents the asynchronous read operation. The value of the type parameter of the value task + /// contains the number of characters that have been read, or 0 if at the end of the stream and no data was read. + /// The number will be less than or equal to the length, depending on whether the data is + /// available within the stream. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readasync#system-io-textreader-readasync(system-memory((system-char))-system-threading-cancellationtoken)")] + public static ValueTask ReadAsync( + this TextReader target, + Memory buffer, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.ReadAsync(segment.Array!, segment.Offset, segment.Count)); + } +#endif + +#if !NET7_0_OR_GREATER + /// + /// Reads all characters from the current position to the end of the stream asynchronously and returns them as one string. + /// + /// The token to monitor for cancellation requests. + /// A task that represents the asynchronous read operation. The value of the TResult parameter contains + /// a string with the characters from the current position to the end of the stream. + /// The number of characters is larger than . + /// The stream reader has been disposed. + /// The reader is currently in use by a previous read operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readtoendasync(system-threading-cancellationtoken)")] + public static Task ReadToEndAsync( + this TextReader target, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return target.ReadToEndAsync(); + } + + /// + /// Reads a line of characters asynchronously and returns the data as a string. + /// + /// The token to monitor for cancellation requests. + /// A value task that represents the asynchronous read operation. The value of the TResult + /// parameter contains the next line from the text reader, or is if all of the characters have been read. + /// The number of characters in the next line is larger than . + /// The text reader has been disposed. + /// The reader is currently in use by a previous read operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readlineasync(system-threading-cancellationtoken)")] + public static Task ReadLineAsync( + this TextReader target, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return target.ReadLineAsync(); + } +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_TextWriter.cs b/src/Polyfill/Polyfill_TextWriter.cs new file mode 100644 index 00000000..8ccd9d6d --- /dev/null +++ b/src/Polyfill/Polyfill_TextWriter.cs @@ -0,0 +1,120 @@ +// + +#pragma warning disable + +#if MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0) + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; +using Link = System.ComponentModel.DescriptionAttribute; +using System.Threading; +using System.Threading.Tasks; + +static partial class Polyfill +{ + +#if TASKSEXTENSIONSREFERENCED + + /// + /// Asynchronously writes a character memory region to the stream. + /// + /// The character memory region to write to the stream. + /// + /// The token to monitor for cancellation requests. + /// The default value is . + /// + /// A task that represents the asynchronous write operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)")] + public static ValueTask WriteAsync( + this TextWriter target, + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) + { + // StreamReader doesn't accept cancellation token (pre-netstd2.1) + cancellationToken.ThrowIfCancellationRequested(); + + if (!MemoryMarshal.TryGetArray(buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.WriteAsync(segment.Array!, segment.Offset, segment.Count)); + } + + /// + /// Asynchronously writes the text representation of a character memory region to the stream, followed by a line terminator. + /// + /// The character memory region to write to the stream. + /// + /// The token to monitor for cancellation requests. + /// The default value is . + /// + /// A task that represents the asynchronous write operation. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync#system-io-textwriter-writelineasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)")] + public static ValueTask WriteLineAsync( + this TextWriter target, + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) + { + // StreamReader doesn't accept cancellation token (pre-netstd2.1) + cancellationToken.ThrowIfCancellationRequested(); + + if (!MemoryMarshal.TryGetArray(buffer, out var segment)) + { + segment = new(buffer.ToArray()); + } + + return new(target.WriteLineAsync(segment.Array!, segment.Offset, segment.Count)); + } + +#endif + + /// + /// Writes a character span to the text stream. + /// + /// The character span to write. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))")] + public static void Write( + this TextWriter target, + ReadOnlySpan buffer) + { + var pool = ArrayPool.Shared; + var array = pool.Rent(buffer.Length); + + try + { + buffer.CopyTo(new(array)); + target.Write(array, 0, buffer.Length); + } + finally + { + pool.Return(array); + } + } + + /// + /// Writes the text representation of a character span to the text stream, followed by a line terminator. + /// + /// The char span value to write to the text stream. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeline#system-io-textwriter-writeline(system-readonlyspan((system-char)))")] + public static void WriteLine( + this TextWriter target, + ReadOnlySpan buffer) + { + var pool = ArrayPool.Shared; + var array = pool.Rent(buffer.Length); + + try + { + buffer.CopyTo(new(array)); + target.WriteLine(array, 0, buffer.Length); + } + finally + { + pool.Return(array); + } + } +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Polyfill_TryFormat.cs b/src/Polyfill/Polyfill_TryFormat.cs new file mode 100644 index 00000000..f88b8e42 --- /dev/null +++ b/src/Polyfill/Polyfill_TryFormat.cs @@ -0,0 +1,384 @@ +// + +#pragma warning disable + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + #if MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X) + + /// + /// Tries to format the value of the current instance as UTF-8 into the provided span of bytes. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.timespan.tryformat#system-timespan-tryformat(system-span((system-byte))-system-int32@-system-readonlyspan((system-char))-system-iformatprovider)")] + public static bool TryFormat(this TimeSpan target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? formatProvider = null) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(null, formatProvider); + } + else + { + result = target.ToString(format.ToString(), formatProvider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + /// + /// Tries to format the value of the current instance as UTF-8 into the provided span of bytes. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.guid.tryformat#system-guid-tryformat(system-span((system-char))-system-int32@-system-readonlyspan((system-char)))")] + public static bool TryFormat(this Guid target, Span destination, out int charsWritten, ReadOnlySpan format = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(null); + } + else + { + result = target.ToString(format.ToString()); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.sbyte.tryformat")] + public static bool TryFormat( + this sbyte target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.byte.tryformat")] + public static bool TryFormat(this byte target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.int16.tryformat")] + public static bool TryFormat(this short target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.uint16.tryformat")] + public static bool TryFormat(this ushort target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.int32.tryformat")] + public static bool TryFormat(this int target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.uint32.tryformat")] + public static bool TryFormat(this uint target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.int64.tryformat")] + public static bool TryFormat(this long target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.uint64.tryformat")] + public static bool TryFormat(this ulong target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.single.tryformat")] + public static bool TryFormat(this float target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.double.tryformat")] + public static bool TryFormat(this double target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.decimal.tryformat")] + public static bool TryFormat(this decimal target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.boolean.tryformat")] + public static bool TryFormat(this bool target, Span destination, out int charsWritten) + { + var result = target.ToString(); + return CopyToSpan(destination, out charsWritten, result); + } +#endif + +#if (MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X)) || NET6_0 + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tryformat")] + public static bool TryFormat(this DateTimeOffset target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.datetime.tryformat")] + public static bool TryFormat(this DateTime target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + +#endif +#if NET6_0 + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.dateonly.tryformat")] + public static bool TryFormat(this DateOnly target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.timeonly.tryformat")] + public static bool TryFormat(this TimeOnly target, Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = default) + { + string result; + + if (format.Length == 0) + { + result = target.ToString(provider); + } + else + { + result = target.ToString(format.ToString(), provider); + } + + return CopyToSpan(destination, out charsWritten, result); + } +#endif + +#if NET6_0 || (MEMORYREFERENCED && (NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X)) + static bool CopyToSpan(Span destination, out int charsWritten, string result) + { + if (result.Length == 0) + { + charsWritten = 0; + return true; + } + + charsWritten = result.Length; + return result.TryCopyTo(destination); + } +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Polyfill_Type.cs b/src/Polyfill/Polyfill_Type.cs new file mode 100644 index 00000000..d8e79ce5 --- /dev/null +++ b/src/Polyfill/Polyfill_Type.cs @@ -0,0 +1,60 @@ +// + +#pragma warning disable + +using System; +using System.Reflection; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ +#if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0 + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.hassamemetadatadefinitionas")] + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } +#endif + + /// + /// Gets a value that indicates whether the current Type represents a type parameter in the definition of a generic method. + /// + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.type.isgenericmethodparameter")] + public static bool IsGenericMethodParameter(this Type target) + { +#if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0 + return target.IsGenericParameter && + target.DeclaringMethod != null; +#else + return target.IsGenericMethodParameter; +#endif + } + +#if !NET6_0_OR_GREATER + + /// + /// Searches for the MemberInfo on the current Type that matches the specified MemberInfo. + /// + /// The MemberInfo to find on the current Type. + /// The MemberInfo to find on the current Type. + /// An object representing the member on the current Type that matches the specified member. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.type.getmemberwithsamemetadatadefinitionas")] + internal static MemberInfo GetMemberWithSameMetadataDefinitionAs( + this Type type, + MemberInfo member) + { + const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; + foreach (var info in type.GetMembers(all)) + { + if (info.HasSameMetadataDefinitionAs(member)) + { + return info; + } + } + + throw new MissingMemberException(type.FullName, member.Name); + } + +#endif +} \ No newline at end of file diff --git a/src/Polyfill/Regex/Polyfill_Regex.cs b/src/Polyfill/Regex/Polyfill_Regex.cs new file mode 100644 index 00000000..8be2f191 --- /dev/null +++ b/src/Polyfill/Regex/Polyfill_Regex.cs @@ -0,0 +1,73 @@ +// + +#pragma warning disable + +#if !NET7_0_OR_GREATER && HAS_SPAN + +using System; +using System.Text.RegularExpressions; +using Link = System.ComponentModel.DescriptionAttribute; + +static partial class Polyfill +{ + /// + /// Indicates whether the regular expression specified in the Regex constructor finds a match in a specified input span. + /// + /// true if the regular expression finds a match; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-int32)")] + public static bool IsMatch(this Regex target, ReadOnlySpan input, int startat) + { + return target.IsMatch(input.ToString(), startat); + } + + /// + /// Indicates whether the regular expression specified in the Regex constructor finds a match in a specified input span. + /// + /// true if the regular expression finds a match; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char)))")] + public static bool IsMatch(this Regex target, ReadOnlySpan input) + { + return target.IsMatch(input.ToString()); + } + + /// + /// Searches an input span for all occurrences of a regular expression and returns a Regex.ValueMatchEnumerator to iterate over the matches. + /// + /// A Regex.ValueMatchEnumerator to iterate over the matches. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char)))")] + public static ValueMatchEnumerator EnumerateMatches (this Regex target, ReadOnlySpan input) => + new(target, input, target.RightToLeft ? input.Length : 0); + + /// + /// Searches an input span for all occurrences of a regular expression and returns a Regex.ValueMatchEnumerator to iterate over the matches. + /// + /// A Regex.ValueMatchEnumerator to iterate over the matches. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char))-system-int32)")] + public static ValueMatchEnumerator EnumerateMatches (this Regex target, ReadOnlySpan input, int startat) => + new(target, input, startat); + // + // /// + // /// Searches an input span for all occurrences of a regular expression and returns a Regex.ValueMatchEnumerator to iterate over the matches. + // /// + // /// A Regex.ValueMatchEnumerator to iterate over the matches. + // [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char))-system-string)")] + // public static ValueMatchEnumerator EnumerateMatches (this Regex target, ReadOnlySpan input, string pattern) => + // new(target, input, target.RightToLeft ? input.Length : 0); + // + // /// + // /// Searches an input span for all occurrences of a regular expression and returns a Regex.ValueMatchEnumerator to iterate over the matches. + // /// + // /// A Regex.ValueMatchEnumerator to iterate over the matches. + // [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions)")] + // public static ValueMatchEnumerator EnumerateMatches (this Regex target, ReadOnlySpan input, string pattern, System.Text.RegularExpressions.RegexOptions options) => + // new(target, input, target.RightToLeft ? input.Length : 0); + // + // /// + // /// Searches an input span for all occurrences of a regular expression and returns a Regex.ValueMatchEnumerator to iterate over the matches. + // /// + // /// A Regex.ValueMatchEnumerator to iterate over the matches. + // [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.enumeratematches#system-text-regularexpressions-regex-enumeratematches(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions-system-timespan)")] + // public static ValueMatchEnumerator EnumerateMatches (this Regex target, ReadOnlySpan input, string pattern, System.Text.RegularExpressions.RegexOptions options, TimeSpan matchTimeout) => + // new(target, input, target.RightToLeft ? input.Length : 0); +} +#endif \ No newline at end of file diff --git a/src/Polyfill/Regex/RegexPolyfill.cs b/src/Polyfill/Regex/RegexPolyfill.cs new file mode 100644 index 00000000..e618a13b --- /dev/null +++ b/src/Polyfill/Regex/RegexPolyfill.cs @@ -0,0 +1,58 @@ +// + +#pragma warning disable +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using Link = System.ComponentModel.DescriptionAttribute; + +[ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +static partial class RegexPolyfill +{ +#if HAS_SPAN + /// + /// Indicates whether the specified regular expression finds a match in the specified input span, using the specified matching options and time-out interval. + /// + /// true if the regular expression finds a match; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions-system-timespan)")] + public static bool IsMatch(ReadOnlySpan input, string pattern, RegexOptions options, TimeSpan matchTimeout) + { +#if NET7_0_OR_GREATER + return Regex.IsMatch(input, pattern, options, matchTimeout); +#else + return Regex.IsMatch(input.ToString(), pattern, options, matchTimeout); +#endif + } + + /// + /// Indicates whether the specified regular expression finds a match in the specified input span, using the specified matching options. + /// + /// true if the regular expression finds a match; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string-system-text-regularexpressions-regexoptions)")] + public static bool IsMatch(ReadOnlySpan input, string pattern, System.Text.RegularExpressions.RegexOptions options) + { +#if NET7_0_OR_GREATER + return Regex.IsMatch(input, pattern, options); +#else + return Regex.IsMatch(input.ToString(), pattern, options); +#endif + } + + /// + /// Indicates whether the specified regular expression finds a match in the specified input span. + /// + /// true if the regular expression finds a match; otherwise, false. + [Link("https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string)")] + public static bool IsMatch(ReadOnlySpan input, string pattern) + { +#if NET7_0_OR_GREATER + return Regex.IsMatch(input, pattern); +#else + return Regex.IsMatch(input.ToString(), pattern); +#endif + } +#endif +} diff --git a/src/Polyfill/Regex/ValueMatch.cs b/src/Polyfill/Regex/ValueMatch.cs new file mode 100644 index 00000000..94fd453f --- /dev/null +++ b/src/Polyfill/Regex/ValueMatch.cs @@ -0,0 +1,50 @@ +// + +#pragma warning disable + +#if !NET7_0_OR_GREATER && HAS_SPAN + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.RegularExpressions; + +/// +/// Represents the results from a single regular expression match. +/// +/// +/// The type is immutable and has no public constructor. An instance of the struct is returned by the +/// method when iterating over the results from calling . +/// +[ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +readonly ref struct ValueMatch +{ + private readonly int _index; + private readonly int _length; + + /// + /// Crates an instance of the type based on the passed in and . + /// + /// The position in the original span where the first character of the captured sliced span is found. + /// The length of the captured sliced span. + internal ValueMatch(int index, int length) + { + _index = index; + _length = length; + } + + /// + /// Gets the position in the original span where the first character of the captured sliced span is found. + /// + public int Index => _index; + + /// + /// Gets the length of the captured sliced span. + /// + public int Length => _length; +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/Regex/ValueMatchEnumerator.cs b/src/Polyfill/Regex/ValueMatchEnumerator.cs new file mode 100644 index 00000000..8c079ba3 --- /dev/null +++ b/src/Polyfill/Regex/ValueMatchEnumerator.cs @@ -0,0 +1,79 @@ +// + +#pragma warning disable + +#if !NET7_0_OR_GREATER && HAS_SPAN + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.RegularExpressions; + +/// +/// Represents an enumerator containing the set of successful matches found by iteratively applying a regular expression pattern to the input span. +/// +/// +/// The enumerator has no public constructor. The method returns a +/// object.The enumerator will lazily iterate over zero or more objects. If there is at least one successful match in the span, then +/// returns and will contain the first . If there are no successful matches, +/// then returns and throws an . +/// +/// This type is a ref struct since it stores the input span as a field in order to be able to lazily iterate over it. +/// +[ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +ref struct ValueMatchEnumerator +{ + ReadOnlySpan _input; + ValueMatch _current; + MatchCollection matchCollection; + int index = 0; + + /// + /// Creates an instance of the for the passed in which iterates over . + /// + /// The to use for finding matches. + /// The input span to iterate over. + /// The position where the engine should start looking for matches from. + internal ValueMatchEnumerator(Regex regex, ReadOnlySpan input, int startAt) + { + matchCollection = regex.Matches(input.ToString(), startAt); + _input = input; + _current = default; + } + + /// + /// Provides an enumerator that iterates through the matches in the input span. + /// + /// A copy of this enumerator. + public readonly ValueMatchEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next match in the span. + /// + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator cannot find additional matches. + /// + public bool MoveNext() + { + if (index == matchCollection.Count) + { + return false; + } + + var match = matchCollection[index]; + _current = new ValueMatch(match.Index, match.Length); + index++; + return true; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + /// Enumeration has either not started or has already finished. + public readonly ValueMatch Current => _current; +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/RequiredMemberAttribute.cs b/src/Polyfill/RequiredMemberAttribute.cs index d74a11ea..144e1218 100644 --- a/src/Polyfill/RequiredMemberAttribute.cs +++ b/src/Polyfill/RequiredMemberAttribute.cs @@ -1,15 +1,12 @@ -#if !NET7_0_OR_GREATER +// -#pragma warning disable +#if !NET7_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.CompilerServices; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/ResolveHttpGlobalProblem.cs b/src/Polyfill/ResolveHttpGlobalProblem.cs new file mode 100644 index 00000000..9c05a616 --- /dev/null +++ b/src/Polyfill/ResolveHttpGlobalProblem.cs @@ -0,0 +1,15 @@ +// + +#if NETFRAMEWORK + +using System.ComponentModel; + +namespace System.Net.Http +{ + [EditorBrowsable(EditorBrowsableState.Never)] + static class ResolveHttpGlobalProblem + { + } +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/SetsRequiredMembersAttribute.cs b/src/Polyfill/SetsRequiredMembersAttribute.cs index 634ad6e1..1af9596a 100644 --- a/src/Polyfill/SetsRequiredMembersAttribute.cs +++ b/src/Polyfill/SetsRequiredMembersAttribute.cs @@ -1,9 +1,6 @@ -#if !NET7_0_OR_GREATER - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if !NET7_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/SkipLocalsInitAttribute.cs b/src/Polyfill/SkipLocalsInitAttribute.cs index 4e3b2ba1..7f64aa8b 100644 --- a/src/Polyfill/SkipLocalsInitAttribute.cs +++ b/src/Polyfill/SkipLocalsInitAttribute.cs @@ -1,15 +1,12 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.CompilerServices; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/StackTraceHiddenAttribute.cs b/src/Polyfill/StackTraceHiddenAttribute.cs index 22e45c99..028b9bab 100644 --- a/src/Polyfill/StackTraceHiddenAttribute.cs +++ b/src/Polyfill/StackTraceHiddenAttribute.cs @@ -1,14 +1,11 @@ -#if !NET6_0_OR_GREATER +// -#pragma warning disable +#if !NET6_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Diagnostics; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/StringInterpolation/AppendInterpolatedStringHandler.cs b/src/Polyfill/StringInterpolation/AppendInterpolatedStringHandler.cs new file mode 100644 index 00000000..fa58329e --- /dev/null +++ b/src/Polyfill/StringInterpolation/AppendInterpolatedStringHandler.cs @@ -0,0 +1,350 @@ +// + +#if HAS_SPAN && !NET6_0_OR_GREATER + +#nullable enable + +namespace System.Text; + +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// Provides a handler used by the language compiler to append interpolated strings into instances. +[EditorBrowsable(EditorBrowsableState.Never)] +[InterpolatedStringHandler] +[ExcludeFromCodeCoverage] +#if PolyPublic +public +#endif +struct AppendInterpolatedStringHandler +{ + // Implementation note: + // As this type is only intended to be targeted by the compiler, public APIs eschew argument validation logic + // in a variety of places, e.g. allowing a null input when one isn't expected to produce a NullReferenceException rather + // than an ArgumentNullException. + + private const int StackallocCharBufferSizeLimit = 256; + + /// The associated StringBuilder to which to append. + private readonly StringBuilder _stringBuilder; + + /// Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls. + private readonly IFormatProvider? _provider; + + /// Whether provides an ICustomFormatter. + /// + /// Custom formatters are very rare. We want to support them, but it's ok if we make them more expensive + /// in order to make them as pay-for-play as possible. So, we avoid adding another reference type field + /// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider + /// provides a formatter, rather than actually storing the formatter. This in turn means, if there is a + /// formatter, we pay for the extra interface call on each AppendFormatted that needs it. + /// + private readonly bool _hasCustomFormatter; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated StringBuilder to which to append. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public AppendInterpolatedStringHandler(int literalLength, int formattedCount, StringBuilder stringBuilder) + { + _stringBuilder = stringBuilder; + _provider = null; + _hasCustomFormatter = false; + } + + /// Creates a handler used to translate an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated StringBuilder to which to append. + /// An object that supplies culture-specific formatting information. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public AppendInterpolatedStringHandler(int literalLength, int formattedCount, StringBuilder stringBuilder, IFormatProvider? provider) + { + _stringBuilder = stringBuilder; + _provider = provider; + _hasCustomFormatter = provider is not null && DefaultInterpolatedStringHandler.HasCustomFormatter(provider); + } + + /// Writes the specified string to the handler. + /// The string to write. + public void AppendLiteral(string value) => _stringBuilder.Append(value); + + #region AppendFormatted + + // Design note: + // This provides the same set of overloads and semantics as DefaultInterpolatedStringHandler. + + #region AppendFormatted T + + /// Writes the specified value to the handler. + /// The value to write. + /// The type of the value to write. + public void AppendFormatted(T value) + { + // This method could delegate to AppendFormatted with a null format, but explicitly passing + // default as the format to TryFormat helps to improve code quality in some cases when TryFormat is inlined, + // e.g. for Int32 it enables the JIT to eliminate code in the inlined method based on a length check on the format. + + if (_hasCustomFormatter) + { + // If there's a custom formatter, always use it. + AppendCustomFormatter(value, format: null); + } + else if (value is IFormattable fValue) + { + // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter + // requires the former. For value types, it won't matter as the type checks devolve into + // JIT-time constants. For reference types, they're more likely to implement IFormattable + // than they are to implement ISpanFormattable: if they don't implement either, we save an + // interface check over first checking for ISpanFormattable and then for IFormattable, and + // if it only implements IFormattable, we come out even: only if it implements both do we + // end up paying for an extra interface check. + + if (typeof(T).IsEnum || HasTryFormatExtension(typeof(T)) || fValue is ISpanFormattable) + { + // Formats into temporary space and then copies the result into the StringBuilder. + AppendFormattedWithTempSpace(value, 0, format: null); + } + else + { + // constrained call avoiding boxing for value types + _stringBuilder.Append(fValue.ToString(format: null, _provider)); + } + } + else if (value is not null) + { + _stringBuilder.Append(value.ToString()); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// The type of the value to write. + public void AppendFormatted(T value, string? format) + { + if (_hasCustomFormatter) + { + // If there's a custom formatter, always use it. + AppendCustomFormatter(value, format); + } + else if (value is IFormattable fValue) + { + // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter + // requires the former. For value types, it won't matter as the type checks devolve into + // JIT-time constants. For reference types, they're more likely to implement IFormattable + // than they are to implement ISpanFormattable: if they don't implement either, we save an + // interface check over first checking for ISpanFormattable and then for IFormattable, and + // if it only implements IFormattable, we come out even: only if it implements both do we + // end up paying for an extra interface check. + + if (typeof(T).IsEnum || HasTryFormatExtension(typeof(T)) || fValue is ISpanFormattable) + { + // Formats into temporary space and then copies the result into the StringBuilder. + AppendFormattedWithTempSpace(value, 0, format); + } + else + { + // constrained call avoiding boxing for value types + _stringBuilder.Append(fValue.ToString(format, _provider)); + } + } + else if (value is not null) + { + _stringBuilder.Append(value.ToString()); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The type of the value to write. + public void AppendFormatted(T value, int alignment) => + AppendFormatted(value, alignment, format: null); + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The type of the value to write. + public void AppendFormatted(T value, int alignment, string? format) + { + if (alignment == 0) + { + // This overload is used as a fallback from several disambiguation overloads, so special-case 0. + AppendFormatted(value, format); + } + else if (alignment < 0) + { + // Left aligned: format into the handler, then append any additional padding required. + var start = _stringBuilder.Length; + AppendFormatted(value, format); + var paddingRequired = -alignment - (_stringBuilder.Length - start); + if (paddingRequired > 0) + { + _stringBuilder.Append(' ', paddingRequired); + } + } + else + { + // Right aligned: format into temporary space and then copy that into the handler, appropriately aligned. + AppendFormattedWithTempSpace(value, alignment, format); + } + } + + /// Formats into temporary space and then appends the result into the StringBuilder. + private void AppendFormattedWithTempSpace(T value, int alignment, string? format) + { + // It's expected that either there's not enough space in the current chunk to store this formatted value, + // or we have a non-0 alignment that could require padding inserted. So format into temporary space and + // then append that written span into the StringBuilder: StringBuilder.Append(span) is able to split the + // span across the current chunk and any additional chunks required. + + var handler = new DefaultInterpolatedStringHandler(0, 0, _provider, stackalloc char[StackallocCharBufferSizeLimit]); + handler.AppendFormatted(value, format); + AppendFormatted(handler.Text, alignment); + handler.Clear(); + } + + #endregion + + #region AppendFormatted ReadOnlySpan + + /// Writes the specified character span to the handler. + /// The span to write. + public void AppendFormatted(ReadOnlySpan value) => _stringBuilder.Append(value); + + /// Writes the specified string of chars to the handler. + /// The span to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The format string. + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) + { + if (alignment == 0) + { + _stringBuilder.Append(value); + } + else + { + var leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + var paddingRequired = alignment - value.Length; + if (paddingRequired <= 0) + { + _stringBuilder.Append(value); + } + else if (leftAlign) + { + _stringBuilder.Append(value); + _stringBuilder.Append(' ', paddingRequired); + } + else + { + _stringBuilder.Append(' ', paddingRequired); + _stringBuilder.Append(value); + } + } + } + + #endregion + + #region AppendFormatted string + + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(string? value) + { + if (!_hasCustomFormatter) + { + _stringBuilder.Append(value); + } + else + { + AppendFormatted(value); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The format string. + public void AppendFormatted(string? value, int alignment = 0, string? format = null) => + // Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload + // simply to disambiguate between ROS and object, just in case someone does specify a format, as + // string is implicitly convertible to both. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + + #endregion + + #region AppendFormatted object + + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The format string. + public void AppendFormatted(object? value, int alignment = 0, string? format = null) => + // This overload is expected to be used rarely, only if either a) something strongly typed as object is + // formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It + // exists purely to help make cases from (b) compile. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + + #endregion + + #endregion + + /// Formats the value using the custom formatter from the provider. + /// The value to write. + /// The format string. + /// The type of the value to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendCustomFormatter(T value, string? format) + { + // This case is very rare, but we need to handle it prior to the other checks in case + // a provider was used that supplied an ICustomFormatter which wanted to intercept the particular value. + // We do the cast here rather than in the ctor, even though this could be executed multiple times per + // formatting, to make the cast pay for play. + Debug.Assert(_hasCustomFormatter); + Debug.Assert(_provider != null); + + var formatter = (ICustomFormatter?)_provider!.GetFormat(typeof(ICustomFormatter)); + Debug.Assert(formatter != null, "An incorrectly written provider said it implemented ICustomFormatter, and then didn't"); + + if (formatter is not null) + { + _stringBuilder.Append(formatter.Format(format, value, _provider)); + } + } + + private static bool HasTryFormatExtension(Type type) + { + return type == typeof(int) || type == typeof(bool) || type == typeof(byte) || type == typeof(float) || + type == typeof(double) || type == typeof(DateTime) || type == typeof(DateTimeOffset) || + type == typeof(decimal) || type == typeof(long) || type == typeof(short) || type == typeof(ushort) || + type == typeof(uint) || type == typeof(ulong) || type == typeof(sbyte); + } +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/StringInterpolation/DefaultInterpolatedStringHandler.cs b/src/Polyfill/StringInterpolation/DefaultInterpolatedStringHandler.cs new file mode 100644 index 00000000..6120c3b2 --- /dev/null +++ b/src/Polyfill/StringInterpolation/DefaultInterpolatedStringHandler.cs @@ -0,0 +1,816 @@ +// + +#if HAS_SPAN && !NET6_0_OR_GREATER + +#nullable enable + +namespace System.Runtime.CompilerServices; + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using Link = System.ComponentModel.DescriptionAttribute; + +/// Provides a handler used by the language compiler to process interpolated strings into instances. +[InterpolatedStringHandler] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.defaultinterpolatedstringhandler")] +#if PolyPublic +public +#endif +ref struct DefaultInterpolatedStringHandler +{ + // Implementation note: + // As this type lives in CompilerServices and is only intended to be targeted by the compiler, + // public APIs eschew argument validation logic in a variety of places, e.g. allowing a null input + // when one isn't expected to produce a NullReferenceException rather than an ArgumentNullException. + + /// Expected average length of formatted data used for an individual interpolation expression result. + /// + /// This is inherited from string.Format, and could be changed based on further data. + /// string.Format actually uses `format.Length + args.Length * 8`, but format.Length + /// includes the format items themselves, e.g. "{0}", and since it's rare to have double-digit + /// numbers of items, we bump the 8 up to 11 to account for the three extra characters in "{d}", + /// since the compiler-provided base length won't include the equivalent character count. + /// + private const int GuessedLengthPerHole = 11; + /// Minimum size array to rent from the pool. + /// Same as stack-allocation size used today by string.Format. + private const int MinimumArrayPoolLength = 256; + + /// Maximum length allowed for a string. + /// Keep in sync with AllocateString in gchelpers.cpp. + private const int StringMaxLength = 0x3FFFFFDF; + + /// Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls. + private readonly IFormatProvider? _provider; + /// Array rented from the array pool and used to back . + private char[]? _arrayToReturnToPool; + /// The span to write into. + private Span _chars; + /// Position at which to write the next character. + private int _pos; + /// Whether provides an ICustomFormatter. + /// + /// Custom formatters are very rare. We want to support them, but it's ok if we make them more expensive + /// in order to make them as pay-for-play as possible. So, we avoid adding another reference type field + /// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider + /// provides a formatter, rather than actually storing the formatter. This in turn means, if there is a + /// formatter, we pay for the extra interface call on each AppendFormatted that needs it. + /// + private readonly bool _hasCustomFormatter; + + /// Creates a handler used to translate an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public DefaultInterpolatedStringHandler(int literalLength, int formattedCount) + { + _provider = null; + _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(GetDefaultLength(literalLength, formattedCount)); + _pos = 0; + _hasCustomFormatter = false; + } + + /// Creates a handler used to translate an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// An object that supplies culture-specific formatting information. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider) + { + _provider = provider; + _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(GetDefaultLength(literalLength, formattedCount)); + _pos = 0; + _hasCustomFormatter = provider is not null && HasCustomFormatter(provider); + } + + /// Creates a handler used to translate an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// An object that supplies culture-specific formatting information. + /// A buffer temporarily transferred to the handler for use as part of its formatting. Contents may be overwritten. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Span initialBuffer) + { + _provider = provider; + _chars = initialBuffer; + _arrayToReturnToPool = null; + _pos = 0; + _hasCustomFormatter = provider is not null && HasCustomFormatter(provider); + } + + /// Derives a default length with which to seed the handler. + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant + internal static int GetDefaultLength(int literalLength, int formattedCount) => + Math.Max(MinimumArrayPoolLength, literalLength + formattedCount * GuessedLengthPerHole); + + /// Gets the built . + /// The built string. + public override string ToString() => Text.ToString(); + + /// Gets the built and clears the handler. + /// The built string. + /// + /// This releases any resources used by the handler. The method should be invoked only + /// once and as the last thing performed on the handler. Subsequent use is erroneous, ill-defined, + /// and may destabilize the process, as may using any other copies of the handler after ToStringAndClear + /// is called on any one of them. + /// + public string ToStringAndClear() + { + var result = Text.ToString(); + Clear(); + return result; + } + + /// Clears the handler, returning any rented array to the pool. + [MethodImpl(MethodImplOptions.AggressiveInlining)] // used only on a few hot paths + internal void Clear() + { + var toReturn = _arrayToReturnToPool; + this = default; // defensive clear + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + /// Gets a span of the written characters thus far. + internal ReadOnlySpan Text => _chars.Slice(0, _pos); + + /// Writes the specified string to the handler. + /// The string to write. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AppendLiteral(string value) + { + if (value.TryCopyTo(_chars.Slice(_pos))) + { + _pos += value.Length; + } + else + { + GrowThenCopyString(value); + } + } + + #region AppendFormatted + // Design note: + // The compiler requires a AppendFormatted overload for anything that might be within an interpolation expression; + // if it can't find an appropriate overload, for handlers in general it'll simply fail to compile. + // (For target-typing to string where it uses DefaultInterpolatedStringHandler implicitly, it'll instead fall back to + // its other mechanisms, e.g. using string.Format. This fallback has the benefit that if we miss a case, + // interpolated strings will still work, but it has the downside that a developer generally won't know + // if the fallback is happening and they're paying more.) + // + // At a minimum, then, we would need an overload that accepts: + // (object value, int alignment = 0, string? format = null) + // Such an overload would provide the same expressiveness as string.Format. However, this has several + // shortcomings: + // - Every value type in an interpolation expression would be boxed. + // - ReadOnlySpan could not be used in interpolation expressions. + // - Every AppendFormatted call would have three arguments at the call site, bloating the IL further. + // - Every invocation would be more expensive, due to lack of specialization, every call needing to account + // for alignment and format, etc. + // + // To address that, we could just have overloads for T and ReadOnlySpan: + // (T) + // (T, int alignment) + // (T, string? format) + // (T, int alignment, string? format) + // (ReadOnlySpan) + // (ReadOnlySpan, int alignment) + // (ReadOnlySpan, string? format) + // (ReadOnlySpan, int alignment, string? format) + // but this also has shortcomings: + // - Some expressions that would have worked with an object overload will now force a fallback to string.Format + // (or fail to compile if the handler is used in places where the fallback isn't provided), because the compiler + // can't always target type to T, e.g. `b switch { true => 1, false => null }` where `b` is a bool can successfully + // be passed as an argument of type `object` but not of type `T`. + // - Reference types get no benefit from going through the generic code paths, and actually incur some overheads + // from doing so. + // - Nullable value types also pay a heavy price, in particular around interface checks that would generally evaporate + // at compile time for value types but don't (currently) if the Nullable goes through the same code paths + // (see https://github.com/dotnet/runtime/issues/50915). + // + // We could try to take a more elaborate approach for DefaultInterpolatedStringHandler, since it is the most common + // handler and we want to minimize overheads both at runtime and in IL size, e.g. have a complete set of overloads + // for each of: + // (T, ...) where T : struct + // (T?, ...) where T : struct + // (object, ...) + // (ReadOnlySpan, ...) + // (string, ...) + // but this also has shortcomings, most importantly: + // - If you have an unconstrained T that happens to be a value type, it'll now end up getting boxed to use the object + // overload. This also necessitates the T? overload, since nullable value types don't meet a T : struct constraint, + // so without those they'd all map to the object overloads as well. + // - Any reference type with an implicit cast to ROS will fail to compile due to ambiguities between the + // overloads. string is one such type, hence needing dedicated overloads for it that can be bound to more tightly. + // + // A middle ground we've settled on, which is likely to be the right approach for most other handlers as well, + // would be the set: + // (T, ...) with no constraint + // (ReadOnlySpan) and (ReadOnlySpan, int) + // (object, int alignment = 0, string? format = null) + // (string) and (string, int) + // This would address most of the concerns, at the expense of: + // - Most reference types going through the generic code paths and so being a bit more expensive. + // - Nullable types being more expensive until https://github.com/dotnet/runtime/issues/50915 is addressed. + // We could choose to add a T? where T : struct set of overloads if necessary. + // Strings don't require their own overloads here, but as they're expected to be very common and as we can + // optimize them in several ways (can copy the contents directly, don't need to do any interface checks, don't + // need to pay the shared generic overheads, etc.) we can add overloads specifically to optimize for them. + // + // Hole values are formatted according to the following policy: + // 1. If an IFormatProvider was supplied and it provides an ICustomFormatter, use ICustomFormatter.Format (even if + // the value is null). + // 2. If the type implements ISpanFormattable, use ISpanFormattable.TryFormat. + // 3. If the type implements IFormattable, use IFormattable.ToString. + // 4. Otherwise, use object.ToString. + // This matches the behavior of string.Format, StringBuilder.AppendFormat, etc. The only overloads for which this + // doesn't apply is ReadOnlySpan, which isn't supported by either string.Format nor StringBuilder.AppendFormat, + // but more importantly which can't be boxed to be passed to ICustomFormatter.Format. + + #region AppendFormatted T + /// Writes the specified value to the handler. + /// The value to write. + /// The type of the value to write. + public void AppendFormatted(T value) + { + // This method could delegate to AppendFormatted with a null format, but explicitly passing + // default as the format to TryFormat helps to improve code quality in some cases when TryFormat is inlined, + // e.g. for Int32 it enables the JIT to eliminate code in the inlined method based on a length check on the format. + + // If there's a custom formatter, always use it. + if (_hasCustomFormatter) + { + AppendCustomFormatter(value, format: null); + return; + } + + // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter + // requires the former. For value types, it won't matter as the type checks devolve into + // JIT-time constants. For reference types, they're more likely to implement IFormattable + // than they are to implement ISpanFormattable: if they don't implement either, we save an + // interface check over first checking for ISpanFormattable and then for IFormattable, and + // if it only implements IFormattable, we come out even: only if it implements both do we + // end up paying for an extra interface check. + string? s; + if (value is IFormattable fValue) + { + // If the value can format itself directly into our buffer, do so. + + if (TryFormatWithExtensions(value, default)) + { + return; + } + else if (fValue is ISpanFormattable sfValue) + { + int charsWritten; + // constrained call avoiding boxing for value types + while (!sfValue.TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider)) + { + Grow(); + } + + _pos += charsWritten; + return; + } + + s = fValue.ToString(format: null, _provider); // constrained call avoiding boxing for value types + } + else + { + s = value?.ToString(); + } + + if (s is not null) + { + AppendLiteral(s); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// The type of the value to write. + public void AppendFormatted(T value, string? format) + { + // If there's a custom formatter, always use it. + if (_hasCustomFormatter) + { + AppendCustomFormatter(value, format); + return; + } + + // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter + // requires the former. For value types, it won't matter as the type checks devolve into + // JIT-time constants. For reference types, they're more likely to implement IFormattable + // than they are to implement ISpanFormattable: if they don't implement either, we save an + // interface check over first checking for ISpanFormattable and then for IFormattable, and + // if it only implements IFormattable, we come out even: only if it implements both do we + // end up paying for an extra interface check. + string? s; + if (value is IFormattable fValue) + { + // If the value can format itself directly into our buffer, do so. + + if (TryFormatWithExtensions(value, format.AsSpan())) + { + return; + } + else if (fValue is ISpanFormattable sfValue) + { + int charsWritten; + // constrained call avoiding boxing for value types + while (!sfValue.TryFormat(_chars.Slice(_pos), out charsWritten, format.AsSpan(), _provider)) + { + Grow(); + } + + _pos += charsWritten; + return; + } + + s = fValue.ToString(format, _provider); // constrained call avoiding boxing for value types + } + else + { + s = value?.ToString(); + } + + if (s is not null) + { + AppendLiteral(s); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The type of the value to write. + public void AppendFormatted(T value, int alignment) + { + var startingPos = _pos; + AppendFormatted(value); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The type of the value to write. + public void AppendFormatted(T value, int alignment, string? format) + { + var startingPos = _pos; + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + #endregion + + #region AppendFormatted ReadOnlySpan + /// Writes the specified character span to the handler. + /// The span to write. + public void AppendFormatted(scoped ReadOnlySpan value) + { + // Fast path for when the value fits in the current buffer + if (value.TryCopyTo(_chars.Slice(_pos))) + { + _pos += value.Length; + } + else + { + GrowThenCopySpan(value); + } + } + + /// Writes the specified string of chars to the handler. + /// The span to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The format string. + public void AppendFormatted(scoped ReadOnlySpan value, int alignment = 0, string? format = null) + { + var leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + var paddingRequired = alignment - value.Length; + if (paddingRequired <= 0) + { + // The value is as large or larger than the required amount of padding, + // so just write the value. + AppendFormatted(value); + return; + } + + // Write the value along with the appropriate padding. + EnsureCapacityForAdditionalChars(value.Length + paddingRequired); + if (leftAlign) + { + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + _chars.Slice(_pos, paddingRequired).Fill(' '); + _pos += paddingRequired; + } + else + { + _chars.Slice(_pos, paddingRequired).Fill(' '); + _pos += paddingRequired; + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + } + #endregion + + #region AppendFormatted string + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(string? value) + { + // Fast-path for no custom formatter and a non-null string that fits in the current destination buffer. + if (!_hasCustomFormatter && + value is not null && + value.TryCopyTo(_chars.Slice(_pos))) + { + _pos += value.Length; + } + else + { + AppendFormattedSlow(value); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Slow path to handle a custom formatter, potentially null value, + /// or a string that doesn't fit in the current buffer. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendFormattedSlow(string? value) + { + if (_hasCustomFormatter) + { + AppendCustomFormatter(value, format: null); + } + else if (value is not null) + { + EnsureCapacityForAdditionalChars(value.Length); + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The format string. + public void AppendFormatted(string? value, int alignment = 0, string? format = null) => + // Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload + // simply to disambiguate between ROS and object, just in case someone does specify a format, as + // string is implicitly convertible to both. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + #endregion + + #region AppendFormatted object + /// Writes the specified value to the handler. + /// The value to write. + /// + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates + /// left-aligned and the required minimum is the absolute value. + /// + /// The format string. + public void AppendFormatted(object? value, int alignment = 0, string? format = null) => + // This overload is expected to be used rarely, only if either a) something strongly typed as object is + // formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It + // exists purely to help make cases from (b) compile. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + #endregion + #endregion + + /// Gets whether the provider provides a custom formatter. + [MethodImpl(MethodImplOptions.AggressiveInlining)] // only used in a few hot path call sites + internal static bool HasCustomFormatter(IFormatProvider provider) + { + Debug.Assert(provider is not null); + Debug.Assert( + provider is not CultureInfo || provider.GetFormat(typeof(ICustomFormatter)) is null, + "Expected CultureInfo to not provide a custom formatter"); + + return + provider!.GetType() != typeof(CultureInfo) && // optimization to avoid GetFormat in the majority case + provider.GetFormat(typeof(ICustomFormatter)) != null; + } + + /// Formats the value using the custom formatter from the provider. + /// The value to write. + /// The format string. + /// The type of the value to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendCustomFormatter(T value, string? format) + { + // This case is very rare, but we need to handle it prior to the other checks in case + // a provider was used that supplied an ICustomFormatter which wanted to intercept the particular value. + // We do the cast here rather than in the ctor, even though this could be executed multiple times per + // formatting, to make the cast pay for play. + Debug.Assert(_hasCustomFormatter); + Debug.Assert(_provider != null); + + var formatter = (ICustomFormatter?)_provider!.GetFormat(typeof(ICustomFormatter)); + Debug.Assert( + formatter != null, + "An incorrectly written provider said it implemented ICustomFormatter, and then didn't"); + + if (formatter?.Format(format, value, _provider) is { } customFormatted) + { + AppendLiteral(customFormatted); + } + } + + /// Handles adding any padding required for aligning a formatted value in an interpolation expression. + /// The position at which the written value started. + /// + /// Non-zero minimum number of characters that should be written for this value. If the value is negative, it + /// indicates left-aligned and the required minimum is the absolute value. + /// + private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment) + { + Debug.Assert(startingPos >= 0 && startingPos <= _pos); + Debug.Assert(alignment != 0); + + var charsWritten = _pos - startingPos; + + var leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + var paddingNeeded = alignment - charsWritten; + if (paddingNeeded > 0) + { + EnsureCapacityForAdditionalChars(paddingNeeded); + + if (leftAlign) + { + _chars.Slice(_pos, paddingNeeded).Fill(' '); + } + else + { + _chars.Slice(startingPos, charsWritten).CopyTo(_chars.Slice(startingPos + paddingNeeded)); + _chars.Slice(startingPos, paddingNeeded).Fill(' '); + } + + _pos += paddingNeeded; + } + } + + /// + /// Ensures has the capacity to store beyond . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacityForAdditionalChars(int additionalChars) + { + if (_chars.Length - _pos < additionalChars) + { + Grow(additionalChars); + } + } + + /// + /// Fallback for fast path in when there's not enough space in the destination. + /// + /// The string to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowThenCopyString(string value) + { + Grow(value.Length); + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + /// + /// Fallback for for when not enough space exists in the current buffer. + /// + /// The span to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowThenCopySpan(scoped ReadOnlySpan value) + { + Grow(value.Length); + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + /// + /// Grows to have the capacity to store at least + /// beyond . + /// + [MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible + private void Grow(int additionalChars) + { + // This method is called when the remaining space (_chars.Length - _pos) is + // insufficient to store a specific number of additional characters. Thus, we + // need to grow to at least that new total. GrowCore will handle growing by more + // than that if possible. + Debug.Assert(additionalChars > _chars.Length - _pos); + GrowCore((uint)_pos + (uint)additionalChars); + } + + /// Grows the size of . + [MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible + private void Grow() => + // This method is called when the remaining space in _chars isn't sufficient to continue + // the operation. Thus, we need at least one character beyond _chars.Length. GrowCore + // will handle growing by more than that if possible. + GrowCore((uint)_chars.Length + 1); + + /// + /// Grow the size of to at least the specified . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] // but reuse this grow logic directly in both of the above grow routines + private void GrowCore(uint requiredMinCapacity) + { + // We want the max of how much space we actually required and doubling our capacity (without going beyond + // the max allowed length). We also want to avoid asking for small arrays, to reduce the number of times we + // need to grow, and since we're working with unsigned ints that could technically overflow if someone tried + // to, for example, append a huge string to a huge string, we also clamp to int.MaxValue. + // Even if the array creation fails in such a case, we may later fail in ToStringAndClear. + + var newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, StringMaxLength)); + var arraySize = (int)InternalMath.Clamp(newCapacity, MinimumArrayPoolLength, int.MaxValue); + + var newArray = ArrayPool.Shared.Rent(arraySize); + _chars.Slice(0, _pos).CopyTo(newArray); + + var toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = newArray; + + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + private bool TryFormatWithExtensions(T value, ReadOnlySpan format) + { + int charsWritten; + switch (value) + { + case int cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case bool cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten)) + { + Grow(); + } + break; + case byte cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case float cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case double cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case DateTime cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case DateTimeOffset cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case decimal cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case long cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case short cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case ushort cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case uint cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case ulong cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + case sbyte cval: + while (!cval.TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) + { + Grow(); + } + break; + default: + return false; + } + + _pos += charsWritten; + return true; + } +} + +[ExcludeFromCodeCoverage] +static file class InternalMath +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Clamp(uint value, uint min, uint max) + { + if (min > max) + { + ThrowMinMaxException(min, max); + } + + if (value < min) + { + return min; + } + else if (value > max) + { + return max; + } + + return value; + } + + [DoesNotReturn] + private static void ThrowMinMaxException(T min, T max) => + throw new ArgumentException(string.Format(SR.Argument_MinMaxValue, min, max)); +} + +[ExcludeFromCodeCoverage] +static file class SR +{ + public const string Argument_MinMaxValue = "'{0}' cannot be greater than {1}."; +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/StringInterpolation/ISpanFormattable.cs b/src/Polyfill/StringInterpolation/ISpanFormattable.cs new file mode 100644 index 00000000..329c3c39 --- /dev/null +++ b/src/Polyfill/StringInterpolation/ISpanFormattable.cs @@ -0,0 +1,32 @@ +// + +#if HAS_SPAN && !NET6_0_OR_GREATER + +using Link = System.ComponentModel.DescriptionAttribute; + +#nullable enable + +namespace System; + +/// Provides functionality to format the string representation of an object into a span. +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable")] +#if PolyPublic +public +#endif +interface ISpanFormattable : IFormattable +{ + /// Tries to format the value of the current instance into the provided span of characters. + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// if the formatting was successful; otherwise, . + /// + /// An implementation of this interface should produce the same string of characters as an implementation of + /// on the same type. + /// TryFormat should return false only if there is not enough space in the destination buffer. Any other failures should throw an exception. + /// + bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider); +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/InterpolatedStringHandlerArgumentAttribute.cs b/src/Polyfill/StringInterpolation/InterpolatedStringHandlerArgumentAttribute.cs similarity index 85% rename from src/Polyfill/InterpolatedStringHandlerArgumentAttribute.cs rename to src/Polyfill/StringInterpolation/InterpolatedStringHandlerArgumentAttribute.cs index 6907b727..04ee127e 100644 --- a/src/Polyfill/InterpolatedStringHandlerArgumentAttribute.cs +++ b/src/Polyfill/StringInterpolation/InterpolatedStringHandlerArgumentAttribute.cs @@ -1,14 +1,12 @@ -#if !NET6_0_OR_GREATER +// -#pragma warning disable +#if !NET6_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedType.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; - -namespace System.Runtime.CompilerServices; +using Link = System.ComponentModel.DescriptionAttribute; /// /// Indicates which arguments to a method involving an interpolated string handler should be passed to that handler. @@ -16,6 +14,7 @@ namespace System.Runtime.CompilerServices; [ExcludeFromCodeCoverage] [DebuggerNonUserCode] [AttributeUsage(AttributeTargets.Parameter)] +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.interpolatedstringhandlerargumentattribute")] #if PolyPublic public #endif @@ -27,7 +26,7 @@ sealed class InterpolatedStringHandlerArgumentAttribute : /// /// The name of the argument that should be passed to the handler. /// may be used as the name of the receiver in an instance method. - public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = new[] { argument }; + public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = [argument]; /// /// Initializes a new instance of the class. diff --git a/src/Polyfill/InterpolatedStringHandlerAttribute.cs b/src/Polyfill/StringInterpolation/InterpolatedStringHandlerAttribute.cs similarity index 56% rename from src/Polyfill/InterpolatedStringHandlerAttribute.cs rename to src/Polyfill/StringInterpolation/InterpolatedStringHandlerAttribute.cs index 36742c9e..23b0b661 100644 --- a/src/Polyfill/InterpolatedStringHandlerAttribute.cs +++ b/src/Polyfill/StringInterpolation/InterpolatedStringHandlerAttribute.cs @@ -1,18 +1,19 @@ -#if !NET6_0_OR_GREATER +// -#pragma warning disable +#if !NET6_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedType.Global +namespace System.Runtime.CompilerServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; - -namespace System.Runtime.CompilerServices; +using Link = System.ComponentModel.DescriptionAttribute; using Targets = AttributeTargets; -/// Indicates the attributed type is to be used as an interpolated string handler. +/// +/// Indicates the attributed type is to be used as an interpolated string handler. +/// +[Link("https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.interpolatedstringhandlerargumentattribute")] [ExcludeFromCodeCoverage] [DebuggerNonUserCode] [AttributeUsage( diff --git a/src/Polyfill/StringSyntaxAttribute.cs b/src/Polyfill/StringSyntaxAttribute.cs index d22488b2..f773986f 100644 --- a/src/Polyfill/StringSyntaxAttribute.cs +++ b/src/Polyfill/StringSyntaxAttribute.cs @@ -1,16 +1,14 @@ +// + #if !NET7_0_OR_GREATER -#pragma warning disable #nullable enable -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Diagnostics.CodeAnalysis; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Diagnostics.CodeAnalysis; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/SuppressGCTransitionAttribute.cs b/src/Polyfill/SuppressGCTransitionAttribute.cs index e66fd1ca..2ae81cfb 100644 --- a/src/Polyfill/SuppressGCTransitionAttribute.cs +++ b/src/Polyfill/SuppressGCTransitionAttribute.cs @@ -1,15 +1,12 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Runtime.InteropServices; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace System.Runtime.InteropServices; - /// /// An attribute used to indicate a GC transition should be skipped when making an unmanaged function call. /// diff --git a/src/Polyfill/Trimming/DynamicDependencyAttribute.cs b/src/Polyfill/Trimming/DynamicDependencyAttribute.cs index a1c0e7e9..33b1722b 100644 --- a/src/Polyfill/Trimming/DynamicDependencyAttribute.cs +++ b/src/Polyfill/Trimming/DynamicDependencyAttribute.cs @@ -1,10 +1,8 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable -#nullable enable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#nullable enable namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Trimming/DynamicallyAccessedMemberTypes.cs b/src/Polyfill/Trimming/DynamicallyAccessedMemberTypes.cs index 054e7d78..01c035cc 100644 --- a/src/Polyfill/Trimming/DynamicallyAccessedMemberTypes.cs +++ b/src/Polyfill/Trimming/DynamicallyAccessedMemberTypes.cs @@ -1,9 +1,6 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable - -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if !NET5_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Trimming/DynamicallyAccessedMembersAttribute.cs b/src/Polyfill/Trimming/DynamicallyAccessedMembersAttribute.cs index 4bbb2733..610449ec 100644 --- a/src/Polyfill/Trimming/DynamicallyAccessedMembersAttribute.cs +++ b/src/Polyfill/Trimming/DynamicallyAccessedMembersAttribute.cs @@ -1,9 +1,6 @@ -#if !NET5_0_OR_GREATER - -#pragma warning disable +// -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#if !NET5_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis; @@ -30,12 +27,15 @@ namespace System.Diagnostics.CodeAnalysis; [ExcludeFromCodeCoverage] [DebuggerNonUserCode] [AttributeUsage( - validOn: Targets.Field | - Targets.ReturnValue | + validOn: Targets.Class | + Targets.Field | Targets.GenericParameter | + Targets.Interface | + Targets.Method | Targets.Parameter | Targets.Property | - Targets.Method, + Targets.ReturnValue | + Targets.Struct, Inherited = false)] #if PolyPublic public diff --git a/src/Polyfill/Trimming/RequiresDynamicCodeAttribute.cs b/src/Polyfill/Trimming/RequiresDynamicCodeAttribute.cs index eff9a41e..0db5e8a3 100644 --- a/src/Polyfill/Trimming/RequiresDynamicCodeAttribute.cs +++ b/src/Polyfill/Trimming/RequiresDynamicCodeAttribute.cs @@ -1,10 +1,8 @@ -#if !NET7_0_OR_GREATER +// -#pragma warning disable -#nullable enable +#if !NET7_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#nullable enable namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Trimming/RequiresUnreferencedCodeAttribute.cs b/src/Polyfill/Trimming/RequiresUnreferencedCodeAttribute.cs index 35b1ba52..6157fc9b 100644 --- a/src/Polyfill/Trimming/RequiresUnreferencedCodeAttribute.cs +++ b/src/Polyfill/Trimming/RequiresUnreferencedCodeAttribute.cs @@ -1,10 +1,8 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable -#nullable enable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#nullable enable namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/Trimming/UnconditionalSuppressMessageAttribute.cs b/src/Polyfill/Trimming/UnconditionalSuppressMessageAttribute.cs index 8b5262fd..54d77ef9 100644 --- a/src/Polyfill/Trimming/UnconditionalSuppressMessageAttribute.cs +++ b/src/Polyfill/Trimming/UnconditionalSuppressMessageAttribute.cs @@ -1,10 +1,8 @@ -#if !NET5_0_OR_GREATER +// -#pragma warning disable -#nullable enable +#if !NET5_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +#nullable enable namespace System.Diagnostics.CodeAnalysis; diff --git a/src/Polyfill/UnmanagedCallersOnlyAttribute.cs b/src/Polyfill/UnmanagedCallersOnlyAttribute.cs index 889a9638..b8856808 100644 --- a/src/Polyfill/UnmanagedCallersOnlyAttribute.cs +++ b/src/Polyfill/UnmanagedCallersOnlyAttribute.cs @@ -1,18 +1,16 @@ +// + #if !NET5_0_OR_GREATER -#pragma warning disable #nullable enable -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - #pragma warning disable CS0649 namespace System.Runtime.InteropServices; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + /// /// Any method marked with can be directly called from /// native code. The function token can be loaded to a local variable using the address-of operator diff --git a/src/Polyfill/UnreachableException.cs b/src/Polyfill/UnreachableException.cs new file mode 100644 index 00000000..7c03178a --- /dev/null +++ b/src/Polyfill/UnreachableException.cs @@ -0,0 +1,57 @@ +// + +#if !NET7_0_OR_GREATER + +#nullable enable + +namespace System.Diagnostics; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +/// +/// Exception thrown when the program executes an instruction that was thought to be unreachable. +/// +/// +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyPublic +public +#endif +sealed class UnreachableException : Exception +{ + /// + /// Initializes a new instance of the class with the default error message. + /// + public UnreachableException() + : base("The program executed an instruction that was thought to be unreachable.") + { + } + + /// + /// Initializes a new instance of the + /// class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public UnreachableException(string? message) + : base(message) + { + } + + /// + /// Initializes a new instance of the + /// class with a specified error message and a reference to the inner exception that is the cause of + /// this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public UnreachableException(string? message, Exception? innerException) + : base(message, innerException) + { + } +} + +#endif \ No newline at end of file diff --git a/src/Polyfill/UnscopedRefAttribute.cs b/src/Polyfill/UnscopedRefAttribute.cs index 330daefa..a61ed71f 100644 --- a/src/Polyfill/UnscopedRefAttribute.cs +++ b/src/Polyfill/UnscopedRefAttribute.cs @@ -1,14 +1,11 @@ -#if !NET7_0_OR_GREATER +// -#pragma warning disable +#if !NET7_0_OR_GREATER -// ReSharper disable RedundantUsingDirective -// ReSharper disable UnusedMember.Global +namespace System.Diagnostics.CodeAnalysis; using System.Diagnostics; -namespace System.Diagnostics.CodeAnalysis; - using Targets = AttributeTargets; /// diff --git a/src/Polyfill/placeholder.txt b/src/Polyfill/placeholder.txt deleted file mode 100644 index 0ceefd36..00000000 --- a/src/Polyfill/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -to stop the nullability types being pulled into net6 \ No newline at end of file diff --git a/src/PublicTests/PublicTests.csproj b/src/PublicTests/PublicTests.csproj index bb434acf..bcf11617 100644 --- a/src/PublicTests/PublicTests.csproj +++ b/src/PublicTests/PublicTests.csproj @@ -1,35 +1,20 @@ - + net462;net472;net48 $(TargetFrameworks);netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 true - - - Pollyfill\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Nullable\%(RecursiveDir)%(Filename).cs - - - Pollyfill\IndexRange\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Trimming\%(RecursiveDir)%(Filename).cs - - - Pollyfill\PlatformCompatibility\%(RecursiveDir)%(Filename).cs - - - - - - - + + + + + + + - - + + \ No newline at end of file diff --git a/src/Shared.sln.DotSettings b/src/Shared.sln.DotSettings new file mode 100644 index 00000000..7a4ed9eb --- /dev/null +++ b/src/Shared.sln.DotSettings @@ -0,0 +1,173 @@ + + True + True + True + DO_NOT_SHOW + ERROR + ERROR + ERROR + WARNING + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + DO_NOT_SHOW + DO_NOT_SHOW + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + C90+,E79+,S14+ + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + DO_NOT_SHOW + *.received.* + *.verified.* + ERROR + ERROR + DO_NOT_SHOW + ECMAScript 2016 + False + False + <?xml version="1.0" encoding="utf-16"?><Profile name="c# Cleanup"><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JSStringLiteralQuotesDescriptor>True</JSStringLiteralQuotesDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><HtmlReformatCode>True</HtmlReformatCode><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="c# Cleanup" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS></Profile> + ExpressionBody + ExpressionBody + ExpressionBody + False + NEVER + NEVER + False + True + CHOP_ALWAYS + False + CHOP_ALWAYS + False + False + RemoveIndent + RemoveIndent + False + True + True + True + True + True + ERROR + \ No newline at end of file diff --git a/src/TestIncludes.targets b/src/TestIncludes.targets new file mode 100644 index 00000000..1b349f16 --- /dev/null +++ b/src/TestIncludes.targets @@ -0,0 +1,28 @@ + + + + Pollyfill\%(Filename).cs + + + Pollyfill\Nullable\%(RecursiveDir)%(Filename).cs + + + Pollyfill\Nullability\%(RecursiveDir)%(Filename).cs + + + Pollyfill\IndexRange\%(RecursiveDir)%(Filename).cs + + + Pollyfill\StringInterpolation\%(RecursiveDir)%(Filename).cs + + + Pollyfill\Trimming\%(RecursiveDir)%(Filename).cs + + + Pollyfill\PlatformCompatibility\%(RecursiveDir)%(Filename).cs + + + Pollyfill\Regex\%(RecursiveDir)%(Filename).cs + + + \ No newline at end of file diff --git a/src/Tests/BuildApiTest.cs b/src/Tests/BuildApiTest.cs new file mode 100644 index 00000000..66939f0a --- /dev/null +++ b/src/Tests/BuildApiTest.cs @@ -0,0 +1,144 @@ +#if NET8_0 && DEBUG +using System.Diagnostics.CodeAnalysis; +using Mono.Cecil; + +[TestFixture] +class BuildApiTest +{ + static string[] namespacesToClean = + [ + "System.Text.RegularExpressions.", + "System.Diagnostics.", + "System.Collections.Generic.", + "System.Threading.Tasks.", + "System.Threading.", + "System.Net.Http.", + "System.Text.", + "System.IO.", + "System." + ]; + + [Test] + public void Run() + { + var solutionDirectory = SolutionDirectoryFinder.Find(); + var path = Path.Combine(solutionDirectory, "Consume", "bin", "Debug", "netstandard2.0", "Consume.dll"); + var md = Path.Combine(solutionDirectory, "..", "api_list.include.md"); + File.Delete(md); + using var module = ModuleDefinition.ReadModule(path); + var types = module.GetTypes().ToList(); + var extensions = types.Single(_ => _.Name == nameof(Polyfill)); + using var writer = File.CreateText(md); + + writer.WriteLine("### Extension methods"); + writer.WriteLine(); + foreach (var type in PublicMethods(extensions.Methods) + .GroupBy(_ => _.Parameters[0].ParameterType.FullName) + .OrderBy(_ => _.Key)) + { + writer.WriteLine($"#### {GetTypeName(type.Key)}"); + writer.WriteLine(); + foreach (var method in type) + { + WriteSignature(method, writer); + } + + writer.WriteLine(); + writer.WriteLine(); + } + + writer.WriteLine("### Static helpers"); + writer.WriteLine(); + + WriteHelper(types, nameof(EnumPolyfill), writer); + WriteHelper(types, "RegexPolyfill", writer); + } + + static void WriteHelper(List types, string name, StreamWriter writer) + { + var helper = types.Single(_ => _.Name == name); + + writer.WriteLine($"#### {helper.Name}"); + writer.WriteLine(); + foreach (var method in PublicMethods(helper.Methods)) + { + WriteSignature(method, writer); + } + + writer.WriteLine(); + writer.WriteLine(); + } + + static string GetTypeName(string targetType) + { + var name = targetType.Replace("`1", "").Replace("`2", "").Replace("`3", ""); + foreach (var toClean in namespacesToClean) + { + name = name.Replace(toClean, ""); + } + + return name; + } + + static IEnumerable PublicMethods(IEnumerable type) => + type.Where(_ => _ is {IsPublic: true, IsConstructor: false}) + .OrderBy(_ => _.Name); + + static void WriteSignature(MethodDefinition method, StreamWriter writer) + { + var parameters = BuildParameters(method); + var typeArgs = BuildTypeArgs(method); + var signature = $"{GetTypeName(method.ReturnType.FullName)} {method.Name}{typeArgs}({parameters})"; + if (TryGetReference(method, out var reference)) + { + writer.WriteLine($" * `{signature}` [reference]({reference})"); + } + else + { + writer.WriteLine($" * `{signature}`"); + } + } + + static string BuildParameters(MethodDefinition method) + { + IEnumerable parameters; + if (IsExtensionMethod(method)) + { + parameters = method.Parameters.Skip(1); + } + else + { + parameters = method.Parameters; + } + + return string.Join(", ", parameters.Select(_ => GetTypeName(_.ParameterType.FullName))); + } + + static bool IsExtensionMethod(MethodDefinition method) => + method.CustomAttributes.Any(_ => _.AttributeType.Name == "ExtensionAttribute"); + + static string BuildTypeArgs(MethodDefinition method) + { + if (method.HasGenericParameters) + { + return $"<{string.Join(", ", method.GenericParameters.Select(_ => _.Name))}>"; + } + + return ""; + } + + static bool TryGetReference(MethodDefinition method, [NotNullWhen(true)] out string? reference) + { + var descriptionAttribute = method.CustomAttributes + .SingleOrDefault(_ => _.AttributeType.Name == "DescriptionAttribute"); + if (descriptionAttribute == null) + { + reference = null; + return false; + } + + reference = (string) descriptionAttribute.ConstructorArguments.Single().Value!; + return true; + } +} +#endif \ No newline at end of file diff --git a/src/Tests/Guard.cs b/src/Tests/Guard.cs index aa5e0aae..742d5e49 100644 --- a/src/Tests/Guard.cs +++ b/src/Tests/Guard.cs @@ -1,4 +1,5 @@ -// ReSharper disable RedundantUsingDirective +// + #region CallerArgumentExpression using System.IO; @@ -14,7 +15,6 @@ public static void FileExists(string path, [CallerArgumentExpression("path")] st } } - static class GuardUsage { public static string[] Method(string path) diff --git a/src/Tests/MyRecord.cs b/src/Tests/MyRecord.cs index bfefc7bd..a4df030e 100644 --- a/src/Tests/MyRecord.cs +++ b/src/Tests/MyRecord.cs @@ -2,13 +2,7 @@ class InitExample { - private int member; - - public int Member - { - get => member; - init => member = value; - } + public int Member { get; init; } } #endregion \ No newline at end of file diff --git a/src/Tests/NullabilitySamples.cs b/src/Tests/NullabilitySamples.cs new file mode 100644 index 00000000..e6c5ee21 --- /dev/null +++ b/src/Tests/NullabilitySamples.cs @@ -0,0 +1,72 @@ +#pragma warning disable CS0649 +#pragma warning disable CS8618 + +public class NullabilitySamples +{ + #region NullabilityUsage + + [Test] + public void Test() + { + var type = typeof(NullabilityTarget); + var arrayField = type.GetField("ArrayField")!; + var genericField = type.GetField("GenericField")!; + + var context = new NullabilityInfoContext(); + + var arrayInfo = context.Create(arrayField); + + Assert.AreEqual(NullabilityState.NotNull, arrayInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, arrayInfo.ElementType!.ReadState); + + var genericInfo = context.Create(genericField); + + Assert.AreEqual(NullabilityState.NotNull, genericInfo.ReadState); + Assert.AreEqual(NullabilityState.NotNull, genericInfo.GenericTypeArguments[0].ReadState); + Assert.AreEqual(NullabilityState.Nullable, genericInfo.GenericTypeArguments[1].ReadState); + } + + #endregion + + #region NullabilityExtension + + [Test] + public void ExtensionTests() + { + var type = typeof(NullabilityTarget); + var field = type.GetField("StringField")!; + Assert.True(field.IsNullable()); + Assert.AreEqual(NullabilityState.Nullable, field.GetNullability()); + Assert.AreEqual(NullabilityState.Nullable, field.GetNullabilityInfo().ReadState); + } + + #endregion + + // ReSharper disable UnusedMember.Local + // ReSharper disable NotAccessedField.Local + class PropertyTarget + { + string? write; + public string? ReadWrite { get; set; } + public string? Read { get; } + + public string? Write + { + set => write = value; + } + } + // ReSharper restore NotAccessedField.Local + // ReSharper restore UnusedMember.Local + + [Test] + public void Property() + { + var type = typeof(PropertyTarget); + var readWrite = type.GetProperty("ReadWrite")!; + var write = type.GetProperty("Write")!; + var read = type.GetProperty("Read")!; + Assert.True(readWrite.IsNullable()); + Assert.True(write.IsNullable()); + Assert.True(read.IsNullable()); + } +} \ No newline at end of file diff --git a/src/Tests/NullabilitySync.cs b/src/Tests/NullabilitySync.cs new file mode 100644 index 00000000..09f12ad1 --- /dev/null +++ b/src/Tests/NullabilitySync.cs @@ -0,0 +1,118 @@ +#if NET8_0 && DEBUG +using System.Net.Http; + +[TestFixture] +public class NullabilitySync +{ + static string dir; + + static NullabilitySync() + { + var solutionDir = SolutionDirectoryFinder.Find(); + dir = Path.Combine(solutionDir, "PolyFill", "Nullability"); + } + + [Test] + public async Task Run() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + using var client = new HttpClient(); + var infoContext = await client.GetStringAsync("https://raw.githubusercontent.com/dotnet/runtime/main/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs"); + + infoContext = infoContext + .Replace(".IsGenericMethodParameter", ".IsGenericMethodParameter()") + .Replace("SR.NullabilityInfoContext_NotSupported", "\"NullabilityInfoContext is not supported\"") + .Replace( + "CheckNullabilityAttributes(nullability, setter.GetParametersAsSpan()[^1].GetCustomAttributesData());", + """ + var parameters = setter.GetParameters(); + CheckNullabilityAttributes(nullability, parameters[parameters.Length-1].GetCustomAttributesData()); + """) + .Replace("ReadOnlySpan parameters = metaMethod.GetParametersAsSpan();", "var parameters = metaMethod.GetParameters();") + .Replace(".GetParametersAsSpan()", ".GetParameters()"); + + var lines = infoContext.Split('\r', '\n'); + infoContext = string.Join(Environment.NewLine, lines.Where(_ => !_.Contains("ArgumentNullException.ThrowIfNull"))); + + var info = await client.GetStringAsync("https://raw.githubusercontent.com/dotnet/runtime/main/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs"); + + var prefix = """ + // + + #if !NET6_0_OR_GREATER + + #nullable enable + + using System.Linq; + using System.Diagnostics.CodeAnalysis; + + + """; + + var suffix = """ + + #endif + + """; + + infoContext = prefix + infoContext + suffix; + infoContext = MakeInternal(infoContext); + infoContext = AddDebugClassFix(infoContext); + OverWrite(infoContext, "NullabilityInfoContext.cs"); + + info = prefix + info + suffix; + info = MakeInternal(info); + info = AddDebugClassFix(info); + OverWrite(info, "NullabilityInfo.cs"); + } + + static string MakeInternal(string source) => + source + .Replace( + "public enum", + """ + #if PolyPublic + public + #endif + enum + """) + .Replace( + "public sealed class", + """ + [ExcludeFromCodeCoverage] + #if PolyPublic + public + #endif + sealed class + """); + + static string AddDebugClassFix(string source) + { + var newValue = """ + namespace System.Reflection + { + using System.Linq; + using System.Diagnostics.CodeAnalysis; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + + """; + return source + .Replace("namespace System.Reflection\r\n{", newValue) + .Replace("namespace System.Reflection\n{", newValue) + .Replace("namespace System.Reflection\r{", newValue); + } + + static void OverWrite(string content, string file) + { + var path = Path.Combine(dir, file); + File.Delete(path); + File.WriteAllText(path, content.Trim()); + } +} +#endif \ No newline at end of file diff --git a/src/Tests/NullabilityTarget.cs b/src/Tests/NullabilityTarget.cs new file mode 100644 index 00000000..e1edcc98 --- /dev/null +++ b/src/Tests/NullabilityTarget.cs @@ -0,0 +1,10 @@ +#pragma warning disable CS0649, CS8618 + +#region NullabilityTarget +class NullabilityTarget +{ + public string? StringField; + public string?[] ArrayField; + public Dictionary GenericField; +} +#endregion \ No newline at end of file diff --git a/src/Tests/PolyfillExtensionsSample.cs b/src/Tests/PolyfillExtensionsSample.cs deleted file mode 100644 index 058e215c..00000000 --- a/src/Tests/PolyfillExtensionsSample.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ReSharper disable PartialTypeWithSinglePart - -[TestFixture] -partial class PolyfillExtensionsSample -{ - [Test] - public void EndsWith() - { - Assert.True("value".EndsWith('e')); - Assert.False("".EndsWith('e')); - } - - [Test] - public void StringContainsStringComparison() => - Assert.True("value".Contains("E", StringComparison.OrdinalIgnoreCase)); - - [Test] - public void StartsWith() - { - Assert.True("value".StartsWith('v')); - Assert.False("".StartsWith('v')); - } -} diff --git a/src/Tests/PolyfillExtensionsSample_Memory.cs b/src/Tests/PolyfillExtensionsSample_Memory.cs deleted file mode 100644 index 0a74d5a4..00000000 --- a/src/Tests/PolyfillExtensionsSample_Memory.cs +++ /dev/null @@ -1,25 +0,0 @@ -partial class PolyfillExtensionsSample -{ - [Test] - public void SpanContains() => - Assert.True("value".AsSpan().Contains('e')); - - [Test] - public void SpanSequenceEqual() => - Assert.True("value".AsSpan().SequenceEqual("value")); - - [Test] - public void SpanStringBuilderAppend() - { - var builder = new StringBuilder(); - builder.Append("value".AsSpan()); - Assert.AreEqual("value", builder.ToString()); - } - - [Test] - public void StringEqualsSpan() - { - var builder = new StringBuilder("value"); - Assert.IsTrue(builder.Equals("value".AsSpan())); - } -} diff --git a/src/Tests/PolyfillTests.cs b/src/Tests/PolyfillTests.cs new file mode 100644 index 00000000..ab0ace44 --- /dev/null +++ b/src/Tests/PolyfillTests.cs @@ -0,0 +1,5 @@ +// ReSharper disable PartialTypeWithSinglePart + +[TestFixture] +[DebuggerNonUserCode] +partial class PolyfillTests; diff --git a/src/Tests/PolyfillTests_CancellationToken.cs b/src/Tests/PolyfillTests_CancellationToken.cs new file mode 100644 index 00000000..d82880d3 --- /dev/null +++ b/src/Tests/PolyfillTests_CancellationToken.cs @@ -0,0 +1,75 @@ +partial class PolyfillTests +{ + [Test] + public void CancellationToken_Register_Exceptions() + { + Cancel token = default; + +#nullable disable + Assert.Throws(() => token.Register((Action) null, null)); + + // ReSharper disable once RedundantCast + Assert.Throws(() => token.UnsafeRegister((Action) null, null)); + Assert.Throws(() => token.UnsafeRegister((Action) null, null)); +#nullable enable + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public static void CancellationToken_Register_ExecutionContextFlowsIfExpected(bool callbackWithToken) + { + var cancelSource = new CancelSource(); + + const int iterations = 5; + var invoked = 0; + + var asyncLocal = new AsyncLocal(); + for (var i = 1; i <= iterations; i++) + { + var flowExecutionContext = i % 2 == 0; + + asyncLocal.Value = i; + Action callback = s => + { + invoked++; + Assert.AreEqual(flowExecutionContext ? (int) s! : 0, asyncLocal.Value); + }; + + var token = cancelSource.Token; + if (flowExecutionContext && callbackWithToken) + { + token.Register( + (s, t) => + { + Assert.AreEqual(token, t); + callback(s); + }, + i); + } + else if (flowExecutionContext) + { + token.Register(callback, i); + } + else if (callbackWithToken) + { + token.UnsafeRegister( + (s, t) => + { + Assert.AreEqual(token, t); + callback(s); + }, + i); + } + else + { + token.UnsafeRegister(callback, i); + } + } + + asyncLocal.Value = 0; + + cancelSource.Cancel(); + Assert.AreEqual(iterations, invoked); + } +} \ No newline at end of file diff --git a/src/Tests/PolyfillTests_CancellationTokenSource.cs b/src/Tests/PolyfillTests_CancellationTokenSource.cs new file mode 100644 index 00000000..e2e8884e --- /dev/null +++ b/src/Tests/PolyfillTests_CancellationTokenSource.cs @@ -0,0 +1,78 @@ +partial class PolyfillTests +{ + private static bool IsCompletedSuccessfully(Task task) + { +#if NETFRAMEWORK || NETSTANDARD + return task.Status == TaskStatus.RanToCompletion; +#else + return task.IsCompletedSuccessfully; +#endif + } + + [Test] + [Ignore("This test is taken directly from the .NET repo but we can't match the real CancelAsync logic exactly, so differ slightly and can't pass this test")] + public static void CancelSource_CancelAsync_NoRegistrations_CallbackCompletesImmediately() + { + var cancelSource = new CancelSource(); + Assert.True(IsCompletedSuccessfully(cancelSource.CancelAsync())); + Assert.True(cancelSource.IsCancellationRequested); + + cancelSource = new(); + cancelSource.Token + .Register( + () => + { + }) + .Dispose(); + Assert.True(IsCompletedSuccessfully(cancelSource.CancelAsync())); + Assert.True(cancelSource.IsCancellationRequested); + } + + [Test] + public static async Task CancelSource_CancelAsync_CallbacksInvokedAsynchronously() + { + var cancelSource = new CancelSource(); + + var resetEventSlim = new ManualResetEventSlim(); + cancelSource.Token.Register(resetEventSlim.Wait); + + var t = cancelSource.CancelAsync(); + Assert.False(t.IsCompleted); + Assert.True(cancelSource.IsCancellationRequested); + + // secondary call completes immediately + Assert.True(IsCompletedSuccessfully(cancelSource.CancelAsync())); + + resetEventSlim.Set(); + await t; + } + + [Test] + public static void CancelSource_CancelAsync_AllCallbacksInvoked() + { + const int iterations = 1000; + + var sum = 0; + + var cancelSource = new CancelSource(); + for (var i = 1; i <= iterations; i++) + { + cancelSource.Token.Register( + s => + { + sum += (int) s!; + }, + i); + } + + var t = cancelSource.CancelAsync(); + Assert.True(cancelSource.IsCancellationRequested); + + // synchronously block without inlining to ensure this thread isn't reused + ((IAsyncResult) t).AsyncWaitHandle.WaitOne(); + // propagate any exceptions + t.Wait(cancelSource.Token); + + Assert.AreEqual(iterations * (iterations + 1) / 2, sum); + } +} \ No newline at end of file diff --git a/src/Tests/PolyfillTests_Dictionary.cs b/src/Tests/PolyfillTests_Dictionary.cs new file mode 100644 index 00000000..118802d7 --- /dev/null +++ b/src/Tests/PolyfillTests_Dictionary.cs @@ -0,0 +1,27 @@ +partial class PolyfillTests +{ + [Test] + public void Dictionary_Remove() + { + var dictionary = new Dictionary { {"key", "value"} }; + + Assert.True(dictionary.Remove("key", out var value)); + Assert.AreEqual("value", value); + } + + [Test] + public void Dictionary_Remove_DoesntThrowOnMissingKey() + { + var dictionary = new Dictionary(); + + Assert.False(dictionary.Remove("non-existent key", out var value)); + Assert.AreEqual(default, value); + } + + [Test] + public void Dictionary_Remove_ThrowsOnNull() + { + var dictionary = new Dictionary(); + Assert.Throws(() => dictionary.Remove(null!, out _)); + } +} diff --git a/src/Tests/PolyfillTests_Enum.cs b/src/Tests/PolyfillTests_Enum.cs new file mode 100644 index 00000000..a9f85c88 --- /dev/null +++ b/src/Tests/PolyfillTests_Enum.cs @@ -0,0 +1,16 @@ +partial class PolyfillTests +{ + [Test] + public void EnumGetValues() + { + var dayOfWeeks = EnumPolyfill.GetValues(); + Assert.AreEqual(DayOfWeek.Sunday, dayOfWeeks[0]); + } + + [Test] + public void EnumGetNames() + { + var dayOfWeeks = EnumPolyfill.GetNames(); + Assert.AreEqual("Sunday", dayOfWeeks[0]); + } +} diff --git a/src/Tests/PolyfillTests_HttpClient.cs b/src/Tests/PolyfillTests_HttpClient.cs new file mode 100644 index 00000000..2a13c77a --- /dev/null +++ b/src/Tests/PolyfillTests_HttpClient.cs @@ -0,0 +1,130 @@ +// These tests cover HttpContent polyfills as well + +using System.Net; +using System.Net.Http; + +partial class PolyfillTests +{ + class FakeHttpMessageHandler : HttpMessageHandler + { + protected override Task SendAsync( + HttpRequestMessage request, + Cancel cancellation) + { + cancellation.ThrowIfCancellationRequested(); + + return Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + RequestMessage = request, + Content = new StringContent("Fake Content") + } + ); + } + } + + [Test] + public async Task HttpClientGetStreamAsync_Positive() + { + // Arrange + var cancel = new Cancel(); + using var httpClient = new HttpClient(new FakeHttpMessageHandler(), true); + + // Act + using var stream = await httpClient.GetStreamAsync("https://example.com", cancel); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(cancel); + + // Assert + Assert.AreEqual("Fake Content", content); + } + + [Test] + public async Task HttpClientGetStreamAsync_Negative() + { + // Arrange + var cancel = new Cancel(true); + using var httpClient = new HttpClient(new FakeHttpMessageHandler(), true); + + // Act + try + { + await httpClient.GetStreamAsync("https://example.com", cancel); + Assert.Fail(); + } + catch (OperationCanceledException exception) + { + // Assert + Assert.IsTrue(exception.CancellationToken.IsCancellationRequested); + } + } + + [Test] + public async Task HttpClientGetByteArrayAsync_Positive() + { + // Arrange + var cancel = new Cancel(); + using var httpClient = new HttpClient(new FakeHttpMessageHandler(), true); + + // Act + var bytes = await httpClient.GetByteArrayAsync("https://example.com", cancel); + var content = Encoding.UTF8.GetString(bytes); + + // Assert + Assert.AreEqual("Fake Content", content); + } + + [Test] + public async Task HttpClientGetByteArrayAsync_Negative() + { + // Arrange + var cancel = new Cancel(true); + using var httpClient = new HttpClient(new FakeHttpMessageHandler(), true); + + // Act + try + { + await httpClient.GetByteArrayAsync("https://example.com", cancel); + Assert.Fail(); + } + catch (OperationCanceledException exception) + { + // Assert + Assert.IsTrue(exception.CancellationToken.IsCancellationRequested); + } + } + + [Test] + public async Task HttpClientGetStringAsync_Positive() + { + // Arrange + var cancel = new Cancel(); + using var httpClient = new HttpClient(new FakeHttpMessageHandler(), true); + + // Act + var content = await httpClient.GetStringAsync("https://example.com", cancel); + + // Assert + Assert.AreEqual("Fake Content", content); + } + + [Test] + public async Task HttpClientGetStringAsync_Negative() + { + // Arrange + var cancel = new Cancel(true); + using var httpClient = new HttpClient(new FakeHttpMessageHandler(), true); + + // Act + try + { + await httpClient.GetStringAsync("https://example.com", cancel); + Assert.Fail(); + } + catch (OperationCanceledException exception) + { + // Assert + Assert.IsTrue(exception.CancellationToken.IsCancellationRequested); + } + } +} \ No newline at end of file diff --git a/src/Tests/PolyfillTests_IEnumerable.cs b/src/Tests/PolyfillTests_IEnumerable.cs new file mode 100644 index 00000000..c230ec31 --- /dev/null +++ b/src/Tests/PolyfillTests_IEnumerable.cs @@ -0,0 +1,91 @@ +partial class PolyfillTests +{ + [Test] + public void MaxBy() + { + var enumerable = (IEnumerable)new List {1, 2}; + + Assert.AreEqual(2, enumerable.MaxBy(_ => _)); + } + + [Test] + public void Except() + { + var enumerable = new List {1, 2}; + Assert.AreEqual(1, enumerable.Except(2).Single()); + } + + [Test] + public void MinBy() + { + var enumerable = (IEnumerable)new List {1, 2}; + + Assert.AreEqual(1, enumerable.MinBy(_ => _)); + } + + [Test] + public void IEnumerableAppend() + { + var enumerable = (IEnumerable)new List {"a", "b"}; + + Assert.IsTrue(enumerable.Append("c").SequenceEqual(new List {"a", "b", "c"})); + } + + [Test] + public void IEnumerableSkipLast() + { + var enumerable = (IEnumerable)new List { "a", "b" }; + + Assert.IsTrue(enumerable.SkipLast(1).SequenceEqual(new List { "a" })); + } + + [Test] + public void ToHashSet () + { + var enumerable = (IEnumerable)new List { "a", "b" }; + + var hashSet = enumerable.ToHashSet(); + Assert.IsTrue(hashSet.Contains("a")); + Assert.IsTrue(hashSet.Contains("b")); + } + + [Test] + public void Chunk_SizeOf3() + { + var enumerable = Enumerable.Range(1, 11).ToList(); + + var chunks = enumerable.Chunk(3).ToList(); + + Assert.AreEqual(new[] { 1, 2, 3 }, chunks[0]); + Assert.AreEqual(new[] { 4, 5, 6 }, chunks[1]); + Assert.AreEqual(new[] { 7, 8, 9 }, chunks[2]); + Assert.AreEqual(new[] { 10, 11 }, chunks[3]); + } + + [Test] + public void Chunk_SizeOf8() + { + var enumerable = Enumerable.Range(1, 11).ToList(); + + var chunks = enumerable.Chunk(8).ToList(); + + Assert.AreEqual(new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, chunks[0]); + Assert.AreEqual(new[] { 9, 10, 11 }, chunks[1]); + } + + [Test] + public void Chunk_SizeOfZero_ExpectedException() + { + var enumerable = Enumerable.Range(1, 11).ToList(); + + Assert.Throws(() => enumerable.Chunk(0).ToList()); + } + + [Test] + public void Chunk_Null_ExpectedException() + { + IEnumerable values = null!; + + Assert.Throws(() => values.Chunk(1).ToList()); + } +} diff --git a/src/Tests/PolyfillTests_IReadOnlyDictionary.cs b/src/Tests/PolyfillTests_IReadOnlyDictionary.cs new file mode 100644 index 00000000..02323aa2 --- /dev/null +++ b/src/Tests/PolyfillTests_IReadOnlyDictionary.cs @@ -0,0 +1,15 @@ +partial class PolyfillTests +{ + [Test] + public void IReadOnlyDictionaryGetValueOrDefault() + { + var dictionary = new Dictionary + { + {"key", "value"} + }; + + Assert.AreEqual("value", dictionary.GetValueOrDefault("key")); + Assert.AreEqual(null, dictionary.GetValueOrDefault("key1")); + Assert.AreEqual("value1", dictionary.GetValueOrDefault("key1","value1")); + } +} diff --git a/src/Tests/PolyfillTests_Memory.cs b/src/Tests/PolyfillTests_Memory.cs new file mode 100644 index 00000000..ef5fca80 --- /dev/null +++ b/src/Tests/PolyfillTests_Memory.cs @@ -0,0 +1,292 @@ +partial class PolyfillTests +{ + [Test] + public void ReadOnlySpan_ZeroLengthContains() + { + var span = new ReadOnlySpan(Array.Empty()); + + var found = span.Contains(0); + Assert.False(found); + } + + [Test] + public void ReadOnlySpan_TestContains() + { + for (var length = 0; length < 32; length++) + { + var a = new int[length]; + for (var i = 0; i < length; i++) + { + a[i] = 10 * (i + 1); + } + var span = new ReadOnlySpan(a); + + for (var targetIndex = 0; targetIndex < length; targetIndex++) + { + var target = a[targetIndex]; + var found = span.Contains(target); + Assert.True(found); + } + } + } + + [Test] + public void ReadOnlySpan_TestMultipleContains() + { + for (var length = 2; length < 32; length++) + { + var a = new int[length]; + for (var i = 0; i < length; i++) + { + a[i] = 10 * (i + 1); + } + + a[length - 1] = 5555; + a[length - 2] = 5555; + + var span = new ReadOnlySpan(a); + var found = span.Contains(5555); + Assert.True(found); + } + } + + [Test] + public void ReadOnlySpan_ZeroLengthContains_String() + { + var span = new ReadOnlySpan(Array.Empty()); + var found = span.Contains("a"); + Assert.False(found); + } + + [Test] + public void ReadOnlySpan_TestMatchContains_String() + { + for (var length = 0; length < 32; length++) + { + var a = new string[length]; + for (var i = 0; i < length; i++) + { + a[i] = (10 * (i + 1)).ToString(); + } + + var span = new ReadOnlySpan(a); + + for (var targetIndex = 0; targetIndex < length; targetIndex++) + { + var target = a[targetIndex]; + var found = span.Contains(target); + Assert.True(found); + } + } + } + + [Test] + public void ReadOnlySpan_TestNoMatchContains_String() + { + var rnd = new Random(42); + for (var length = 0; length <= byte.MaxValue; length++) + { + var a = new string[length]; + var target = rnd.Next(0, 256).ToString(); + for (var i = 0; i < length; i++) + { + var val = (i + 1).ToString(); + a[i] = val == target ? target + 1 : val; + } + var span = new ReadOnlySpan(a); + + var found = span.Contains(target); + Assert.False(found); + } + } + + [Test] + public void ReadOnlySpan_TestMultipleMatchContains_String() + { + for (var length = 2; length < 32; length++) + { + var a = new string[length]; + for (var i = 0; i < length; i++) + { + a[i] = (10 * (i + 1)).ToString(); + } + + a[length - 1] = "5555"; + a[length - 2] = "5555"; + + var span = new ReadOnlySpan(a); + var found = span.Contains("5555"); + Assert.True(found); + } + } + + [Test] + public void Span_ZeroLengthContains() + { + var span = new Span(Array.Empty()); + + var found = span.Contains(0); + Assert.False(found); + } + + [Test] + public void Span_TestContains() + { + for (var length = 0; length < 32; length++) + { + var a = new int[length]; + for (var i = 0; i < length; i++) + { + a[i] = 10 * (i + 1); + } + var span = new Span(a); + + for (var targetIndex = 0; targetIndex < length; targetIndex++) + { + var target = a[targetIndex]; + var found = span.Contains(target); + Assert.True(found); + } + } + } + + [Test] + public void Span_TestMultipleContains() + { + for (var length = 2; length < 32; length++) + { + var a = new int[length]; + for (var i = 0; i < length; i++) + { + a[i] = 10 * (i + 1); + } + + a[length - 1] = 5555; + a[length - 2] = 5555; + + var span = new Span(a); + var found = span.Contains(5555); + Assert.True(found); + } + } + + [Test] + public void Span_ZeroLengthContains_String() + { + var span = new Span(Array.Empty()); + var found = span.Contains("a"); + Assert.False(found); + } + + [Test] + public void Span_TestMatchContains_String() + { + for (var length = 0; length < 32; length++) + { + var a = new string[length]; + for (var i = 0; i < length; i++) + { + a[i] = (10 * (i + 1)).ToString(); + } + var span = new Span(a); + + for (var targetIndex = 0; targetIndex < length; targetIndex++) + { + var target = a[targetIndex]; + var found = span.Contains(target); + Assert.True(found); + } + } + } + + [Test] + public void Span_TestNoMatchContains_String() + { + var rnd = new Random(42); + for (var length = 0; length <= byte.MaxValue; length++) + { + var a = new string[length]; + var target = rnd.Next(0, 256).ToString(); + for (var i = 0; i < length; i++) + { + var val = (i + 1).ToString(); + a[i] = val == target ? target + 1 : val; + } + var span = new Span(a); + + var found = span.Contains(target); + Assert.False(found); + } + } + + [Test] + public void Span_TestMultipleMatchContains_String() + { + for (var length = 2; length < 32; length++) + { + var a = new string[length]; + for (var i = 0; i < length; i++) + { + a[i] = (10 * (i + 1)).ToString(); + } + + a[length - 1] = "5555"; + a[length - 2] = "5555"; + + var span = new Span(a); + var found = span.Contains("5555"); + Assert.True(found); + } + } + + [Test] + public void Span_SequenceEqual() + { + Assert.True("value".AsSpan().SequenceEqual("value")); + Assert.False("value".AsSpan().SequenceEqual("value2")); + Assert.False("value".AsSpan().SequenceEqual("v")); + var span = new Span("value".ToCharArray()); + Assert.True(span.SequenceEqual("value")); + Assert.False(span.SequenceEqual("value2")); + Assert.False(span.SequenceEqual("v")); + } + + [Test] + public void Span_StartsWith() + { + Assert.True("value".AsSpan().StartsWith("value")); + Assert.False("value".AsSpan().StartsWith("value2")); + Assert.True("value".AsSpan().StartsWith("v")); + var span = new Span("value".ToCharArray()); + Assert.True(span.StartsWith("value")); + Assert.False(span.StartsWith("value2")); + Assert.True(span.StartsWith("val")); + } + + [Test] + public void Span_EndsWith() + { + Assert.True("value".AsSpan().EndsWith("value")); + Assert.False("value".AsSpan().EndsWith("value2")); + Assert.True("value".AsSpan().EndsWith("e")); + var span = new Span("value".ToCharArray()); + Assert.True(span.EndsWith("value")); + Assert.False(span.EndsWith("value2")); + Assert.True(span.EndsWith("lue")); + } + + [Test] + public void SpanStringBuilderAppend() + { + var builder = new StringBuilder(); + builder.Append("value".AsSpan()); + Assert.AreEqual("value", builder.ToString()); + } + + [Test] + public void StringEqualsSpan() + { + var builder = new StringBuilder("value"); + Assert.IsTrue(builder.Equals("value".AsSpan())); + } +} diff --git a/src/Tests/PolyfillTests_MicroNanosecond.cs b/src/Tests/PolyfillTests_MicroNanosecond.cs new file mode 100644 index 00000000..8c4af238 --- /dev/null +++ b/src/Tests/PolyfillTests_MicroNanosecond.cs @@ -0,0 +1,36 @@ +partial class PolyfillTests +{ + static DateTimeOffset dateTimeOffset = DateTimeOffset.Now; + static DateTime dateTime = DateTime.Now; + + [Test] + public void AddMicroseconds() + { + var fromTicksDateTimeOffset = dateTimeOffset.AddSeconds(1); + var fromMicrosecondsDateTimeOffset = dateTimeOffset.AddMicroseconds(1000000); + Assert.AreEqual(fromTicksDateTimeOffset, fromMicrosecondsDateTimeOffset); + + var fromTicksDateTime = dateTime.AddSeconds(1); + var fromMicrosecondsDateTime = dateTime.AddMicroseconds(1000000); + Assert.AreEqual(fromTicksDateTime, fromMicrosecondsDateTime); + } + +#if NET7_0_OR_GREATER + static TimeSpan timeSpan = DateTime.Now.TimeOfDay; + [Test] + public void Nanoseconds() + { + Assert.AreEqual(dateTimeOffset.Nanosecond, dateTimeOffset.Nanosecond()); + Assert.AreEqual(timeSpan.Nanoseconds, timeSpan.Nanoseconds()); + Assert.AreEqual(dateTime.Nanosecond, dateTime.Nanosecond()); + } + + [Test] + public void Microsecond() + { + Assert.AreEqual(dateTimeOffset.Microsecond, dateTimeOffset.Microsecond()); + Assert.AreEqual(timeSpan.Microseconds, timeSpan.Microseconds()); + Assert.AreEqual(dateTime.Microsecond, dateTime.Microsecond()); + } +#endif +} \ No newline at end of file diff --git a/src/Tests/PolyfillTests_Process.cs b/src/Tests/PolyfillTests_Process.cs new file mode 100644 index 00000000..026e2f90 --- /dev/null +++ b/src/Tests/PolyfillTests_Process.cs @@ -0,0 +1,25 @@ +partial class PolyfillTests +{ + [Test, RequiresThread] + [TestCase(0)] // poll + [TestCase(10)] // real timeout + public void Process_CurrentProcess_WaitAsyncNeverCompletes(int milliseconds) + { + using var cancelSource = new CancelSource(milliseconds); + var cancel = cancelSource.Token; + var process = Process.GetCurrentProcess(); + + var ex = Assert.CatchAsync(() => process.WaitForExitAsync(cancel)) as OperationCanceledException; + + Assert.IsNotNull(ex); + Assert.AreEqual(cancel, ex!.CancellationToken); + Assert.False(process.HasExited); + } + + [Test, RequiresThread] + public void Process_WaitForExitAsync_NotDirected_ThrowsInvalidOperationException() + { + var process = new Process(); + Assert.ThrowsAsync(async () => await process.WaitForExitAsync()); + } +} diff --git a/src/Tests/PolyfillTests_Regex.cs b/src/Tests/PolyfillTests_Regex.cs new file mode 100644 index 00000000..f122740d --- /dev/null +++ b/src/Tests/PolyfillTests_Regex.cs @@ -0,0 +1,28 @@ +using System.Text.RegularExpressions; + +partial class PolyfillTests +{ + [Test] + public void RegexIsMatch() + { + var regex = new Regex(@"\d+"); + var match = regex.Match("a55a"); + Assert.IsTrue(match.Success); + } + + [Test] + public void EnumerateMatches() + { + var regex = new Regex(@"\d+"); + var span = "a55a".AsSpan(); + var found = false; + foreach (var match in regex.EnumerateMatches(span)) + { + found = true; + Assert.AreEqual(1, match.Index); + Assert.AreEqual(2, match.Length); + } + + Assert.IsTrue(found); + } +} diff --git a/src/Tests/PolyfillTests_Stream.cs b/src/Tests/PolyfillTests_Stream.cs new file mode 100644 index 00000000..5450ec50 --- /dev/null +++ b/src/Tests/PolyfillTests_Stream.cs @@ -0,0 +1,14 @@ +partial class PolyfillTests +{ + [Test] + public async Task StreamReadAsyncMemory() + { + var input = new byte[]{1,2}; + using var stream = new MemoryStream(input); + var result = new byte[2]; + var memory = new Memory(result); + var read = await stream.ReadAsync(memory); + Assert.AreEqual(2, read); + Assert.IsTrue(input.SequenceEqual(result)); + } +} diff --git a/src/Tests/PolyfillExtensionsSample_Stream.cs b/src/Tests/PolyfillTests_StreamReader.cs similarity index 53% rename from src/Tests/PolyfillExtensionsSample_Stream.cs rename to src/Tests/PolyfillTests_StreamReader.cs index ad9e503a..9d483420 100644 --- a/src/Tests/PolyfillExtensionsSample_Stream.cs +++ b/src/Tests/PolyfillTests_StreamReader.cs @@ -1,4 +1,4 @@ -partial class PolyfillExtensionsSample +partial class PolyfillTests { [Test] public async Task StreamReaderReadAsync() @@ -13,14 +13,11 @@ public async Task StreamReaderReadAsync() } [Test] - public async Task StreamReadAsync() + public async Task StreamReaderReadToEndAsync() { - var input = new byte[]{1,2}; - using var stream = new MemoryStream(input); - var result = new byte[2]; - var memory = new Memory(result); - var read = await stream.ReadAsync(memory); - Assert.AreEqual(2, read); - Assert.IsTrue(input.SequenceEqual(result)); + using var stream = new MemoryStream("value"u8.ToArray()); + using var reader = new StreamReader(stream); + var read = await reader.ReadToEndAsync(Cancel.None); + Assert.AreEqual("value", read); } } diff --git a/src/Tests/PolyfillTests_String.cs b/src/Tests/PolyfillTests_String.cs new file mode 100644 index 00000000..df100f1a --- /dev/null +++ b/src/Tests/PolyfillTests_String.cs @@ -0,0 +1,58 @@ +// ReSharper disable PartialTypeWithSinglePart + +partial class PolyfillTests +{ + [Test] + public void GetHashCodeStringComparison() + { + var hash = "value".GetHashCode(StringComparison.Ordinal); + Assert.AreNotEqual(0, hash); + } + + [Test] + public void EndsWith() + { + Assert.True("value".EndsWith('e')); + Assert.True("e".EndsWith('e')); + Assert.False("".EndsWith('e')); + } + + [Test] + public void CopyTo() + { + var span = new Span(new char[1]); + "a".CopyTo(span); + Assert.AreEqual("a", span.ToString()); + } + + [Test] + public void TryCopyTo() + { + var span = new Span(new char[1]); + Assert.IsTrue("a".TryCopyTo(span)); + Assert.AreEqual("a", span.ToString()); + } + + [Test] + public void StringContainsStringComparison() => + Assert.True("value".Contains("E", StringComparison.OrdinalIgnoreCase)); + + [Test] + public void StartsWith() + { + Assert.True("value".StartsWith('v')); + Assert.True("v".StartsWith('v')); + Assert.False("".StartsWith('v')); + } + + [Test] + public void Split() + { + Assert.AreEqual(new []{"a","b"}, "a b".Split(' ', StringSplitOptions.RemoveEmptyEntries)); + Assert.AreEqual(new []{"a","b"}, "a b".Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)); + } + + [Test] + public void ContainsChar() => + Assert.True("value".Contains('v')); +} diff --git a/src/Tests/PolyfillTests_StringBuilder.cs b/src/Tests/PolyfillTests_StringBuilder.cs new file mode 100644 index 00000000..521c5142 --- /dev/null +++ b/src/Tests/PolyfillTests_StringBuilder.cs @@ -0,0 +1,69 @@ +// ReSharper disable PartialTypeWithSinglePart + +partial class PolyfillTests +{ + [Test] + public void StringBuilderCopyTo() + { + var builder = new StringBuilder("value"); + + var span = new Span(new char[1]); + builder.CopyTo(0, span, 1); + Assert.True(span is "v"); + + span = new(new char[1]); + builder.CopyTo(1, span, 1); + Assert.True(span is "a"); + + span = new(new char[2]); + builder.CopyTo(1, span, 2); + Assert.True(span is "al"); + + span = new(new char[5]); + builder.CopyTo(0, span, 5); + Assert.True(span is "value"); + } + + [Test] + public void Append() + { + var builder = new StringBuilder(); + + var x = 10; + Polyfill.Append(builder, $"value{x}"); + Assert.AreEqual("value10", builder.ToString()); + } + + [Test] + public void AppendWithFormat() + { + var builder = new StringBuilder(); + + var x = 10; + Polyfill.Append(builder, null, $"value{x}"); + Assert.AreEqual("value10", builder.ToString()); + } + + [Test] + public void AppendJoin() + { + var builder = new StringBuilder(); + + builder.AppendJoin(",", ["value1", "value2"]); + Assert(); + builder.AppendJoin(",", new object[]{"value1", "value2"}); + Assert(); + builder.AppendJoin(',', ["value1", "value2"]); + Assert(); + builder.AppendJoin(',', new object[]{"value1", "value2"}); + Assert(); + builder.AppendJoin(',', ["value1", "value2"]); + Assert(); + + void Assert() + { + NUnit.Framework.Assert.AreEqual("value1,value2", builder.ToString()); + builder.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Tests/PolyfillTests_Task.cs b/src/Tests/PolyfillTests_Task.cs new file mode 100644 index 00000000..fc888fff --- /dev/null +++ b/src/Tests/PolyfillTests_Task.cs @@ -0,0 +1,133 @@ +// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident +// ReSharper disable MethodSupportsCancellation +partial class PolyfillTests +{ + static T? AssertThrowsAsync(string expectedParamName, AsyncTestDelegate action) + where T : ArgumentException + { + var exception = Assert.ThrowsAsync(action); + + Assert.AreEqual(expectedParamName, exception?.ParamName); + + return exception; + } + + [Test] + public void Task_WaitAsync_InvalidTimeout_Throws() + { + foreach (var timeout in new[] { TimeSpan.FromMilliseconds(-2), TimeSpan.MaxValue, TimeSpan.MinValue }) + { +#if NET5_0_OR_GREATER + AssertThrowsAsync("timeout", () => new TaskCompletionSource().Task.WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => new TaskCompletionSource().Task.WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => new TaskCompletionSource().Task.WaitAsync(timeout, new Cancel(true))); +#endif + + AssertThrowsAsync("timeout", () => new TaskCompletionSource().Task.WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => new TaskCompletionSource().Task.WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => new TaskCompletionSource().Task.WaitAsync(timeout, new Cancel(true))); + + AssertThrowsAsync("timeout", () => Task.CompletedTask.WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => Task.CompletedTask.WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => Task.CompletedTask.WaitAsync(timeout, new Cancel(true))); + + AssertThrowsAsync("timeout", () => Task.FromResult(42).WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => Task.FromResult(42).WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => Task.FromResult(42).WaitAsync(timeout, new Cancel(true))); + + AssertThrowsAsync("timeout", () => Task.FromCanceled(new Cancel(true)).WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => Task.FromCanceled(new Cancel(true)).WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => Task.FromCanceled(new Cancel(true)).WaitAsync(timeout, new Cancel(true))); + + AssertThrowsAsync("timeout", () => Task.FromCanceled(new Cancel(true)).WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => Task.FromCanceled(new Cancel(true)).WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => Task.FromCanceled(new Cancel(true)).WaitAsync(timeout, new Cancel(true))); + + AssertThrowsAsync("timeout", () => Task.FromException(new FormatException()).WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => Task.FromException(new FormatException()).WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => Task.FromException(new FormatException()).WaitAsync(timeout, new Cancel(true))); + + AssertThrowsAsync("timeout", () => Task.FromException(new FormatException()).WaitAsync(timeout)); + AssertThrowsAsync("timeout", () => Task.FromException(new FormatException()).WaitAsync(timeout, Cancel.None)); + AssertThrowsAsync("timeout", () => Task.FromException(new FormatException()).WaitAsync(timeout, new Cancel(true))); + } + } + + [Test] + public async Task Task_WaitAsync_CanceledAndTimedOut_AlreadyCompleted_UsesTaskResult() + { + await Task.CompletedTask.WaitAsync(TimeSpan.Zero); + await Task.CompletedTask.WaitAsync(new Cancel(true)); + await Task.CompletedTask.WaitAsync(TimeSpan.Zero, new Cancel(true)); + + Assert.AreEqual(42, await Task.FromResult(42).WaitAsync(TimeSpan.Zero)); + Assert.AreEqual(42, await Task.FromResult(42).WaitAsync(new Cancel(true))); + Assert.AreEqual(42, await Task.FromResult(42).WaitAsync(TimeSpan.Zero, new Cancel(true))); + + Assert.ThrowsAsync(() => Task.FromException(new FormatException()).WaitAsync(TimeSpan.Zero)); + Assert.ThrowsAsync(() => Task.FromException(new FormatException()).WaitAsync(new Cancel(true))); + Assert.ThrowsAsync(() => Task.FromException(new FormatException()).WaitAsync(TimeSpan.Zero, new Cancel(true))); + + Assert.ThrowsAsync(() => Task.FromException(new FormatException()).WaitAsync(TimeSpan.Zero)); + Assert.ThrowsAsync(() => Task.FromException(new FormatException()).WaitAsync(new Cancel(true))); + Assert.ThrowsAsync(() => Task.FromException(new FormatException()).WaitAsync(TimeSpan.Zero, new Cancel(true))); + + Assert.ThrowsAsync(() => Task.FromCanceled(new Cancel(true)).WaitAsync(TimeSpan.Zero)); + Assert.ThrowsAsync(() => Task.FromCanceled(new Cancel(true)).WaitAsync(new Cancel(true))); + Assert.ThrowsAsync(() => Task.FromCanceled(new Cancel(true)).WaitAsync(TimeSpan.Zero, new Cancel(true))); + + Assert.ThrowsAsync(() => Task.FromCanceled(new Cancel(true)).WaitAsync(TimeSpan.Zero)); + Assert.ThrowsAsync(() => Task.FromCanceled(new Cancel(true)).WaitAsync(new Cancel(true))); + Assert.ThrowsAsync(() => Task.FromCanceled(new Cancel(true)).WaitAsync(TimeSpan.Zero, new Cancel(true))); + } + + [Test] + public void Task_WaitAsync_TimeoutOrCanceled_Throws() + { + var tcs = new TaskCompletionSource(); + var cancelSource = new CancelSource(); + + Assert.ThrowsAsync(() => ((Task)tcs.Task).WaitAsync(TimeSpan.Zero)); + Assert.ThrowsAsync(() => ((Task)tcs.Task).WaitAsync(TimeSpan.FromMilliseconds(1))); + Assert.ThrowsAsync(() => ((Task)tcs.Task).WaitAsync(TimeSpan.FromMilliseconds(1), cancelSource.Token)); + + Assert.ThrowsAsync(() => tcs.Task.WaitAsync(TimeSpan.Zero)); + Assert.ThrowsAsync(() => tcs.Task.WaitAsync(TimeSpan.FromMilliseconds(1))); + Assert.ThrowsAsync(() => tcs.Task.WaitAsync(TimeSpan.FromMilliseconds(1), cancelSource.Token)); + + var assert1 = ((Task)tcs.Task).WaitAsync(cancelSource.Token); + var assert2 = ((Task)tcs.Task).WaitAsync(Timeout.InfiniteTimeSpan, cancelSource.Token); + Task assert3 = tcs.Task.WaitAsync(cancelSource.Token); + Task assert4 = tcs.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancelSource.Token); + Assert.False(assert1.IsCompleted); + Assert.False(assert2.IsCompleted); + Assert.False(assert3.IsCompleted); + Assert.False(assert4.IsCompleted); + + cancelSource.Cancel(); + Assert.ThrowsAsync(() => assert1); + Assert.ThrowsAsync(() => assert2); + Assert.ThrowsAsync(() => assert3); + Assert.ThrowsAsync(() => assert4); + } + + [Test] + public async Task Task_WaitAsync_NoCancellationOrTimeoutOccurs_Success() + { + var cancelSource = new CancelSource(); + +#if NET5_0_OR_GREATER + var tcs = new TaskCompletionSource(); + var t = tcs.Task.WaitAsync(TimeSpan.FromDays(1), cancelSource.Token); + Assert.False(t.IsCompleted); + tcs.SetResult(); + await t; +#endif + + var tcsg = new TaskCompletionSource(); + var tg = tcsg.Task.WaitAsync(TimeSpan.FromDays(1), cancelSource.Token); + Assert.False(tg.IsCompleted); + tcsg.SetResult(42); + Assert.AreEqual(42, await tg); + } +} diff --git a/src/Tests/PolyfillTests_TextWriter.cs b/src/Tests/PolyfillTests_TextWriter.cs new file mode 100644 index 00000000..2df80b79 --- /dev/null +++ b/src/Tests/PolyfillTests_TextWriter.cs @@ -0,0 +1,72 @@ +partial class PolyfillTests +{ + [Test] + public async Task TextWriterWriteSpan() + { + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + writer.Write("value".AsSpan()); + await writer.FlushAsync(); + var s = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("value", s); + } + + [Test] + public async Task TextWriterWriteLineSpan() + { + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + writer.WriteLine("value".AsSpan()); + await writer.FlushAsync(); + var s = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("value" + Environment.NewLine, s); + } + + [Test] + public async Task TextWriterWriteMemoryAsync() + { + using var stream = new MemoryStream(); + var memory = new Memory("value".ToArray()); + using var writer = new StreamWriter(stream); + await writer.WriteAsync(memory); + await writer.FlushAsync(); + var s = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("value", s); + } + + [Test] + public async Task TextWriterWriteLineMemoryAsync() + { + using var stream = new MemoryStream(); + var memory = new Memory("value".ToArray()); + using var writer = new StreamWriter(stream); + await writer.WriteLineAsync(memory); + await writer.FlushAsync(); + var s = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("value" + Environment.NewLine, s); + } + + [Test] + public async Task TextWriterWriteMemory() + { + using var stream = new MemoryStream(); + var memory = new Memory("value".ToArray()); + using var writer = new StreamWriter(stream); + writer.Write(memory); + await writer.FlushAsync(); + var s = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("value", s); + } + + [Test] + public async Task TextWriterWriteLineMemory() + { + using var stream = new MemoryStream(); + var memory = new Memory("value".ToArray()); + using var writer = new StreamWriter(stream); + writer.WriteLine(memory); + await writer.FlushAsync(); + var s = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("value" + Environment.NewLine, s); + } +} diff --git a/src/Tests/PolyfillTests_TryFormat.cs b/src/Tests/PolyfillTests_TryFormat.cs new file mode 100644 index 00000000..989ddff5 --- /dev/null +++ b/src/Tests/PolyfillTests_TryFormat.cs @@ -0,0 +1,192 @@ +using System.Globalization; + +partial class PolyfillTests +{ + [Test] + public void TryFormatSByte() + { + sbyte value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatByte() + { + byte value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatInt16() + { + short value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatUInt16() + { + ushort value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatInt32() + { + var value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatUInt32() + { + uint value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatInt64() + { + long value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatUInt64() + { + ulong value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatSingle() + { + float value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatGuid() + { + var value = new Guid("97008c2d-2114-4396-ae19-392c8e6f8f1b"); + Span buffer = stackalloc char[36]; + var result = value.TryFormat(buffer, out var written); + Assert.True(result); + Assert.AreEqual("97008c2d-2114-4396-ae19-392c8e6f8f1b", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatDouble() + { + double value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatDecimal() + { + decimal value = 9; + Span buffer = stackalloc char[1]; + var result = value.TryFormat(buffer, out var written, provider: CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("9", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatDateTimeOffset() + { + var value = new DateTimeOffset(new DateTime(2001, 10, 1), TimeSpan.Zero); + Span buffer = stackalloc char[29]; + var result = value.TryFormat(buffer, out var written, format: "R".AsSpan(), CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("Mon, 01 Oct 2001 00:00:00 GMT", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatDateTime() + { + var value = new DateTime(2001, 10, 1); + Span buffer = stackalloc char[29]; + var result = value.TryFormat(buffer, out var written, format: "R".AsSpan(), CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("Mon, 01 Oct 2001 00:00:00 GMT", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + + [Test] + public void TryFormatBoolean() + { + var value = true; + Span buffer = stackalloc char[4]; + var result = value.TryFormat(buffer, out var written); + Assert.True(result); + Assert.AreEqual("True", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + +#if NET6_0_OR_GREATER + [Test] + public void TryFormatDate() + { + var value = new DateOnly(2001,10,1); + Span buffer = stackalloc char[16]; + var result = value.TryFormat(buffer, out var written, format: "R", CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("Mon, 01 Oct 2001", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } + [Test] + public void TryFormatTime() + { + var value = new TimeOnly(10,1); + Span buffer = stackalloc char[8]; + var result = value.TryFormat(buffer, out var written, format: "R", CultureInfo.InvariantCulture); + Assert.True(result); + Assert.AreEqual("10:01:00", buffer.ToString()); + Assert.AreEqual(buffer.Length, written); + } +#endif +} \ No newline at end of file diff --git a/src/Tests/SanityChecks.cs b/src/Tests/SanityChecks.cs index 9bc98c87..4bc222a2 100644 --- a/src/Tests/SanityChecks.cs +++ b/src/Tests/SanityChecks.cs @@ -35,7 +35,9 @@ public void Attributes() name == "NullableAttribute" || name == "NullableContextAttribute" || name == "IsReadOnlyAttribute" || - name == "RefSafetyRulesAttribute") + name == "RefSafetyRulesAttribute" || + name == "ScopedRefAttribute" || + name == "IsByRefLikeAttribute") { continue; } diff --git a/src/Tests/SolutionDirectoryFinder.cs b/src/Tests/SolutionDirectoryFinder.cs new file mode 100644 index 00000000..d648b3c1 --- /dev/null +++ b/src/Tests/SolutionDirectoryFinder.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; + +static class SolutionDirectoryFinder +{ + public static string Find([CallerFilePath] string sourceFile = "") + { + if (!TryFind(sourceFile, out var solutionDirectory)) + { + throw new("Could not find solution directory"); + } + + return solutionDirectory; + } + + public static bool TryFind(string sourceFile, [NotNullWhen(true)] out string? path) + { + var currentDirectory = Directory.GetParent(sourceFile)!.FullName; + do + { + if (Directory.GetFiles(currentDirectory, "*.sln").Any()) + { + path = currentDirectory; + return true; + } + + var parent = Directory.GetParent(currentDirectory); + if (parent == null) + { + path = null; + return false; + } + + currentDirectory = parent.FullName; + } while (true); + } +} \ No newline at end of file diff --git a/src/Tests/StringInterpolationTests.cs b/src/Tests/StringInterpolationTests.cs new file mode 100644 index 00000000..a39d1443 --- /dev/null +++ b/src/Tests/StringInterpolationTests.cs @@ -0,0 +1,28 @@ +#if HAS_SPAN + +[TestFixture] +public class StringInterpolationTests +{ + [Test] + public void ShouldInterpolateString() + { + Span buffer = stackalloc char[] { 'H', 'e', 'l', 'l', 'o' }; + var number = 15; + var result = $"{buffer}, you're {number} years old"; + + Assert.AreEqual("Hello, you're 15 years old", result); + } + + [Test] + public void ShouldInterpolateStringBuilder() + { + var sb = new StringBuilder(); + Span buffer = stackalloc char[] { 'H', 'e', 'l', 'l', 'o' }; + var number = 15; + Polyfill.Append(sb, $"{buffer}, you're {number} years old {sb.ToString()}"); + var result = sb.ToString(); + + Assert.AreEqual("Hello, you're 15 years old Hello, you're 15 years old ", result); + } +} +#endif \ No newline at end of file diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 51db5641..e9b8d65e 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,34 +1,20 @@ - + net462;net472;net48;net6.0-windows $(TargetFrameworks);netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 - - - Pollyfill\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Nullable\%(RecursiveDir)%(Filename).cs - - - Pollyfill\IndexRange\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Trimming\%(RecursiveDir)%(Filename).cs - - - Pollyfill\PlatformCompatibility\%(RecursiveDir)%(Filename).cs - - - - - - - + + + + + + + - + + - - + + \ No newline at end of file diff --git a/src/Tests/UnreachableExceptionTests.cs b/src/Tests/UnreachableExceptionTests.cs new file mode 100644 index 00000000..732dfbc3 --- /dev/null +++ b/src/Tests/UnreachableExceptionTests.cs @@ -0,0 +1,7 @@ +[TestFixture] +public class UnreachableExceptionTests +{ + [Test] + public void UnreachableException_Compatibility_with_all_TargetFrameworks() => + _ = Assert.Throws(() => throw new UnreachableException()); +} diff --git a/src/UnsafeTests/UnsafeTests.csproj b/src/UnsafeTests/UnsafeTests.csproj index 8c99a93b..4bb9022d 100644 --- a/src/UnsafeTests/UnsafeTests.csproj +++ b/src/UnsafeTests/UnsafeTests.csproj @@ -1,49 +1,20 @@ - + net462;net472;net48 $(TargetFrameworks);netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 True - - - Pollyfill\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Nullable\%(RecursiveDir)%(Filename).cs - - - Pollyfill\IndexRange\%(RecursiveDir)%(Filename).cs - - - Pollyfill\Trimming\%(RecursiveDir)%(Filename).cs - - - Pollyfill\PlatformCompatibility\%(RecursiveDir)%(Filename).cs - - - - - - - - - + + + + + + + + + - - + + \ No newline at end of file diff --git a/src/appveyor.yml b/src/appveyor.yml index 063702e9..7e32c89e 100644 --- a/src/appveyor.yml +++ b/src/appveyor.yml @@ -22,8 +22,8 @@ build_script: sudo ./dotnet-install.sh --jsonfile src/global.json --architecture x64 --install-dir '/usr/share/dotnet' } } -- dotnet build src --configuration Release -- dotnet test src --configuration Release --no-build --no-restore +- dotnet build src --configuration Release --verbosity quiet +- dotnet test src --configuration Release --no-build --no-restore --verbosity quiet on_failure: - ps: Get-ChildItem *.received.* -recurse | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/src/editorconfig.txt b/src/editorconfig.txt new file mode 100644 index 00000000..c9db1cb9 --- /dev/null +++ b/src/editorconfig.txt @@ -0,0 +1,171 @@ +root = true +# EditorConfig: http://EditorConfig.org + +# top-most EditorConfig file + +[*] +indent_style = space + + +[*.cs] +indent_size = 4 +charset = utf-8 + + +# Microsoft .NET properties +trim_trailing_whitespace = true +csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion +resharper_namespace_body = file_scoped +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none + +# ReSharper properties +resharper_object_creation_when_type_not_evident = target_typed + +# ReSharper inspection severities +resharper_arrange_object_creation_when_type_evident_highlighting = error +resharper_arrange_object_creation_when_type_not_evident_highlighting = error +resharper_arrange_redundant_parentheses_highlighting = error +resharper_arrange_static_member_qualifier_highlighting = error +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = none +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_check_namespace_highlighting = none +resharper_convert_to_using_declaration_highlighting = error +resharper_css_not_resolved_highlighting = warning +resharper_field_can_be_made_read_only_local_highlighting = none +resharper_merge_into_logical_pattern_highlighting = warning +resharper_merge_into_pattern_highlighting = error +resharper_method_has_async_overload_highlighting = warning +resharper_partial_type_with_single_part_highlighting = error +resharper_redundant_base_qualifier_highlighting = warning +resharper_redundant_cast_highlighting = error +resharper_redundant_empty_object_creation_argument_list_highlighting = error +resharper_redundant_empty_object_or_collection_initializer_highlighting = error +resharper_redundant_name_qualifier_highlighting = error +resharper_redundant_suppress_nullable_warning_expression_highlighting = error +resharper_redundant_using_directive_highlighting = error +resharper_redundant_verbatim_string_prefix_highlighting = error +resharper_replace_substring_with_range_indexer_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = error +resharper_suggest_var_or_type_elsewhere_highlighting = error +resharper_suggest_var_or_type_simple_types_highlighting = error +resharper_unnecessary_whitespace_highlighting = error +resharper_use_await_using_highlighting = warning +resharper_use_deconstruction_highlighting = warning + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = false:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = true:error + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = true:error +csharp_style_expression_bodied_local_functions = true:error +csharp_style_expression_bodied_constructors = true:error +csharp_style_expression_bodied_operators = true:error +resharper_place_expr_method_on_single_line = false + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:error +csharp_style_expression_bodied_indexers = true:error +csharp_style_expression_bodied_accessors = true:error + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Newline settings +#csharp_new_line_before_open_brace = all:error +resharper_max_array_initializer_elements_on_line = 1 +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +resharper_wrap_before_first_type_parameter_constraint = true +resharper_wrap_extends_list_style = chop_always +resharper_wrap_after_dot_in_method_calls = false +resharper_wrap_before_binary_pattern_op = false +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_place_simple_initializer_on_single_line = false + +dotnet_style_require_accessibility_modifiers = never:error +resharper_place_type_constraints_on_same_line = false +resharper_blank_lines_inside_namespace = 0 +resharper_blank_lines_after_file_scoped_namespace_directive = 1 +resharper_blank_lines_inside_type = 0 + +insert_final_newline = false +resharper_place_attribute_on_same_line = false +resharper_space_around_lambda_arrow = true +resharper_place_constructor_initializer_on_same_line = false + +#braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces +resharper_braces_for_ifelse = required +resharper_braces_for_foreach = required +resharper_braces_for_while = required +resharper_braces_for_dowhile = required +resharper_braces_for_lock = required +resharper_braces_for_fixed = required +resharper_braces_for_for = required + +# Xml files +[*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props}] +indent_size = 2 +# https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi +resharper_blank_line_after_pi = false +resharper_space_before_self_closing = true + +[*.json] +indent_size = 2 \ No newline at end of file diff --git a/src/global.json b/src/global.json index 8b443f2f..b8219f86 100644 --- a/src/global.json +++ b/src/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100-preview.2.23157.25", + "version": "8.0.101", "allowPrerelease": true, "rollForward": "latestFeature" }