Skip to content

Commit 6286403

Browse files
EgorBoCopilot
andcommitted
Narrow the give-up condition for scoped-local-to-parameter pattern
The previous skip (any stackalloc + any parameter assignment) was too broad. The exact CS9080-triggering shape needs three ingredients: 1. a local declared with an explicit `scoped` modifier 2. that local's value assigned to a parameter of the enclosing member 3. the local being populated from a block-scoped initializer (typically stackalloc) Drop any one and the wrap compiles fine. Specifically, the user's `Span<char> escaped = stackalloc char[8]; param = escaped.Slice(...);` shape (no `scoped` on the local) is now correctly reported and transformed - C#'s lenient inference for non-scoped local lifetimes keeps it compilable after wrapping. Analyzer now skips only when the body contains a `VariableDeclarationSyntax` whose `Type` is `ScopedTypeSyntax` AND a simple-assignment expression whose LHS is a parameter name and whose RHS transitively references that scoped local. Verified: 20 tests pass (including one for each ingredient being dropped). STJ regenerated via dotnet format and builds clean (6 files changed, down from 8 - the two newly-touched files were previously over-skipped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 29a17a1 commit 6286403

8 files changed

Lines changed: 476 additions & 391 deletions

File tree

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.TryGetProperty.cs

Lines changed: 153 additions & 143 deletions
Large diffs are not rendered by default.

src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -245,39 +245,44 @@ public static bool TryDecodeBase64InPlace(Span<byte> utf8Unescaped, [NotNullWhen
245245
return true;
246246
}
247247

248-
public static unsafe bool TryDecodeBase64(ReadOnlySpan<byte> utf8Unescaped, [NotNullWhen(true)] out byte[]? bytes)
248+
public static bool TryDecodeBase64(ReadOnlySpan<byte> utf8Unescaped, [NotNullWhen(true)] out byte[]? bytes)
249249
{
250-
byte[]? pooledArray = null;
250+
// SAFETY-TODO: review this unsafe usage.
251+
// Either document why it's safe, refactor to remove it, or mark the enclosing member 'unsafe'.
252+
unsafe
253+
{
254+
byte[]? pooledArray = null;
251255

252-
Span<byte> byteSpan = utf8Unescaped.Length <= JsonConstants.StackallocByteThreshold ?
253-
stackalloc byte[JsonConstants.StackallocByteThreshold] :
254-
(pooledArray = ArrayPool<byte>.Shared.Rent(utf8Unescaped.Length));
256+
Span<byte> byteSpan = utf8Unescaped.Length <= JsonConstants.StackallocByteThreshold ?
257+
stackalloc byte[JsonConstants.StackallocByteThreshold] :
258+
(pooledArray = ArrayPool<byte>.Shared.Rent(utf8Unescaped.Length));
255259

256-
OperationStatus status = Base64.DecodeFromUtf8(utf8Unescaped, byteSpan, out int bytesConsumed, out int bytesWritten);
260+
OperationStatus status = Base64.DecodeFromUtf8(utf8Unescaped, byteSpan, out int bytesConsumed, out int bytesWritten);
257261

258-
if (status != OperationStatus.Done)
259-
{
260-
bytes = null;
262+
if (status != OperationStatus.Done)
263+
{
264+
bytes = null;
265+
266+
if (pooledArray != null)
267+
{
268+
byteSpan.Clear();
269+
ArrayPool<byte>.Shared.Return(pooledArray);
270+
}
271+
272+
return false;
273+
}
274+
Debug.Assert(bytesConsumed == utf8Unescaped.Length);
275+
276+
bytes = byteSpan.Slice(0, bytesWritten).ToArray();
261277

262278
if (pooledArray != null)
263279
{
264280
byteSpan.Clear();
265281
ArrayPool<byte>.Shared.Return(pooledArray);
266282
}
267283

268-
return false;
284+
return true;
269285
}
270-
Debug.Assert(bytesConsumed == utf8Unescaped.Length);
271-
272-
bytes = byteSpan.Slice(0, bytesWritten).ToArray();
273-
274-
if (pooledArray != null)
275-
{
276-
byteSpan.Clear();
277-
ArrayPool<byte>.Shared.Return(pooledArray);
278-
}
279-
280-
return true;
281286
}
282287

283288
public static string TranscodeHelper(ReadOnlySpan<byte> utf8Unescaped)

src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -176,26 +176,31 @@ public static bool TryGetValue(ReadOnlySpan<byte> segment, bool isEscaped, out D
176176
return false;
177177
}
178178

179-
public static unsafe bool TryGetEscapedDateTime(ReadOnlySpan<byte> source, out DateTime value)
179+
public static bool TryGetEscapedDateTime(ReadOnlySpan<byte> source, out DateTime value)
180180
{
181-
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
182-
Span<byte> sourceUnescaped = stackalloc byte[JsonConstants.MaximumEscapedDateTimeOffsetParseLength];
181+
// SAFETY-TODO: review this unsafe usage.
182+
// Either document why it's safe, refactor to remove it, or mark the enclosing member 'unsafe'.
183+
unsafe
184+
{
185+
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
186+
Span<byte> sourceUnescaped = stackalloc byte[JsonConstants.MaximumEscapedDateTimeOffsetParseLength];
183187

184-
Unescape(source, sourceUnescaped, out int written);
185-
Debug.Assert(written > 0);
188+
Unescape(source, sourceUnescaped, out int written);
189+
Debug.Assert(written > 0);
186190

187-
sourceUnescaped = sourceUnescaped.Slice(0, written);
188-
Debug.Assert(!sourceUnescaped.IsEmpty);
191+
sourceUnescaped = sourceUnescaped.Slice(0, written);
192+
Debug.Assert(!sourceUnescaped.IsEmpty);
189193

190-
if (JsonHelpers.IsValidUnescapedDateTimeOffsetParseLength(sourceUnescaped.Length)
191-
&& JsonHelpers.TryParseAsISO(sourceUnescaped, out DateTime tmp))
192-
{
193-
value = tmp;
194-
return true;
195-
}
194+
if (JsonHelpers.IsValidUnescapedDateTimeOffsetParseLength(sourceUnescaped.Length)
195+
&& JsonHelpers.TryParseAsISO(sourceUnescaped, out DateTime tmp))
196+
{
197+
value = tmp;
198+
return true;
199+
}
196200

197-
value = default;
198-
return false;
201+
value = default;
202+
return false;
203+
}
199204
}
200205

201206
public static bool TryGetValue(ReadOnlySpan<byte> segment, bool isEscaped, out DateTimeOffset value)
@@ -224,26 +229,31 @@ public static bool TryGetValue(ReadOnlySpan<byte> segment, bool isEscaped, out D
224229
return false;
225230
}
226231

227-
public static unsafe bool TryGetEscapedDateTimeOffset(ReadOnlySpan<byte> source, out DateTimeOffset value)
232+
public static bool TryGetEscapedDateTimeOffset(ReadOnlySpan<byte> source, out DateTimeOffset value)
228233
{
229-
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
230-
Span<byte> sourceUnescaped = stackalloc byte[JsonConstants.MaximumEscapedDateTimeOffsetParseLength];
234+
// SAFETY-TODO: review this unsafe usage.
235+
// Either document why it's safe, refactor to remove it, or mark the enclosing member 'unsafe'.
236+
unsafe
237+
{
238+
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedDateTimeOffsetParseLength);
239+
Span<byte> sourceUnescaped = stackalloc byte[JsonConstants.MaximumEscapedDateTimeOffsetParseLength];
231240

232-
Unescape(source, sourceUnescaped, out int written);
233-
Debug.Assert(written > 0);
241+
Unescape(source, sourceUnescaped, out int written);
242+
Debug.Assert(written > 0);
234243

235-
sourceUnescaped = sourceUnescaped.Slice(0, written);
236-
Debug.Assert(!sourceUnescaped.IsEmpty);
244+
sourceUnescaped = sourceUnescaped.Slice(0, written);
245+
Debug.Assert(!sourceUnescaped.IsEmpty);
237246

238-
if (JsonHelpers.IsValidUnescapedDateTimeOffsetParseLength(sourceUnescaped.Length)
239-
&& JsonHelpers.TryParseAsISO(sourceUnescaped, out DateTimeOffset tmp))
240-
{
241-
value = tmp;
242-
return true;
243-
}
247+
if (JsonHelpers.IsValidUnescapedDateTimeOffsetParseLength(sourceUnescaped.Length)
248+
&& JsonHelpers.TryParseAsISO(sourceUnescaped, out DateTimeOffset tmp))
249+
{
250+
value = tmp;
251+
return true;
252+
}
244253

245-
value = default;
246-
return false;
254+
value = default;
255+
return false;
256+
}
247257
}
248258

249259
public static bool TryGetValue(ReadOnlySpan<byte> segment, bool isEscaped, out Guid value)
@@ -273,26 +283,31 @@ public static bool TryGetValue(ReadOnlySpan<byte> segment, bool isEscaped, out G
273283
return false;
274284
}
275285

276-
public static unsafe bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
286+
public static bool TryGetEscapedGuid(ReadOnlySpan<byte> source, out Guid value)
277287
{
278-
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedGuidLength);
288+
// SAFETY-TODO: review this unsafe usage.
289+
// Either document why it's safe, refactor to remove it, or mark the enclosing member 'unsafe'.
290+
unsafe
291+
{
292+
Debug.Assert(source.Length <= JsonConstants.MaximumEscapedGuidLength);
279293

280-
Span<byte> utf8Unescaped = stackalloc byte[JsonConstants.MaximumEscapedGuidLength];
281-
Unescape(source, utf8Unescaped, out int written);
282-
Debug.Assert(written > 0);
294+
Span<byte> utf8Unescaped = stackalloc byte[JsonConstants.MaximumEscapedGuidLength];
295+
Unescape(source, utf8Unescaped, out int written);
296+
Debug.Assert(written > 0);
283297

284-
utf8Unescaped = utf8Unescaped.Slice(0, written);
285-
Debug.Assert(!utf8Unescaped.IsEmpty);
298+
utf8Unescaped = utf8Unescaped.Slice(0, written);
299+
Debug.Assert(!utf8Unescaped.IsEmpty);
286300

287-
if (utf8Unescaped.Length == JsonConstants.MaximumFormatGuidLength
288-
&& Utf8Parser.TryParse(utf8Unescaped, out Guid tmp, out _, 'D'))
289-
{
290-
value = tmp;
291-
return true;
292-
}
301+
if (utf8Unescaped.Length == JsonConstants.MaximumFormatGuidLength
302+
&& Utf8Parser.TryParse(utf8Unescaped, out Guid tmp, out _, 'D'))
303+
{
304+
value = tmp;
305+
return true;
306+
}
293307

294-
value = default;
295-
return false;
308+
value = default;
309+
return false;
310+
}
296311
}
297312

298313
#if NET

src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -535,99 +535,104 @@ private bool ConsumeLiteralMultiSegment(ReadOnlySpan<byte> literal, JsonTokenTyp
535535
return true;
536536
}
537537

538-
private unsafe bool CheckLiteralMultiSegment(ReadOnlySpan<byte> span, ReadOnlySpan<byte> literal, out int consumed)
538+
private bool CheckLiteralMultiSegment(ReadOnlySpan<byte> span, ReadOnlySpan<byte> literal, out int consumed)
539539
{
540-
Debug.Assert(span.Length > 0 && span[0] == literal[0] && literal.Length <= JsonConstants.MaximumLiteralLength);
541-
542-
Span<byte> readSoFar = stackalloc byte[JsonConstants.MaximumLiteralLength];
543-
int written = 0;
544-
545-
long prevTotalConsumed = _totalConsumed;
546-
SequencePosition copy = _currentPosition;
547-
if (span.Length >= literal.Length || IsLastSpan)
540+
// SAFETY-TODO: review this unsafe usage.
541+
// Either document why it's safe, refactor to remove it, or mark the enclosing member 'unsafe'.
542+
unsafe
548543
{
549-
_bytePositionInLine += FindMismatch(span, literal);
544+
Debug.Assert(span.Length > 0 && span[0] == literal[0] && literal.Length <= JsonConstants.MaximumLiteralLength);
550545

551-
int amountToWrite = AmountToWrite(span, _bytePositionInLine, readSoFar, written);
552-
span.Slice(0, amountToWrite).CopyTo(readSoFar);
553-
written += amountToWrite;
554-
goto Throw;
555-
}
556-
else
557-
{
558-
if (!literal.StartsWith(span))
546+
Span<byte> readSoFar = stackalloc byte[JsonConstants.MaximumLiteralLength];
547+
int written = 0;
548+
549+
long prevTotalConsumed = _totalConsumed;
550+
SequencePosition copy = _currentPosition;
551+
if (span.Length >= literal.Length || IsLastSpan)
559552
{
560553
_bytePositionInLine += FindMismatch(span, literal);
554+
561555
int amountToWrite = AmountToWrite(span, _bytePositionInLine, readSoFar, written);
562556
span.Slice(0, amountToWrite).CopyTo(readSoFar);
563557
written += amountToWrite;
564558
goto Throw;
565559
}
560+
else
561+
{
562+
if (!literal.StartsWith(span))
563+
{
564+
_bytePositionInLine += FindMismatch(span, literal);
565+
int amountToWrite = AmountToWrite(span, _bytePositionInLine, readSoFar, written);
566+
span.Slice(0, amountToWrite).CopyTo(readSoFar);
567+
written += amountToWrite;
568+
goto Throw;
569+
}
566570

567-
ReadOnlySpan<byte> leftToMatch = literal.Slice(span.Length);
571+
ReadOnlySpan<byte> leftToMatch = literal.Slice(span.Length);
568572

569-
SequencePosition startPosition = _currentPosition;
570-
int startConsumed = _consumed;
571-
int alreadyMatched = literal.Length - leftToMatch.Length;
572-
while (true)
573-
{
574-
_totalConsumed += alreadyMatched;
575-
_bytePositionInLine += alreadyMatched;
576-
if (!GetNextSpan())
573+
SequencePosition startPosition = _currentPosition;
574+
int startConsumed = _consumed;
575+
int alreadyMatched = literal.Length - leftToMatch.Length;
576+
while (true)
577577
{
578-
_totalConsumed = prevTotalConsumed;
579-
consumed = default;
580-
_currentPosition = copy;
581-
if (IsLastSpan)
578+
_totalConsumed += alreadyMatched;
579+
_bytePositionInLine += alreadyMatched;
580+
if (!GetNextSpan())
582581
{
583-
goto Throw;
582+
_totalConsumed = prevTotalConsumed;
583+
consumed = default;
584+
_currentPosition = copy;
585+
if (IsLastSpan)
586+
{
587+
goto Throw;
588+
}
589+
return false;
584590
}
585-
return false;
586-
}
587591

588-
int amountToWrite = Math.Min(span.Length, readSoFar.Length - written);
589-
span.Slice(0, amountToWrite).CopyTo(readSoFar.Slice(written));
590-
written += amountToWrite;
592+
int amountToWrite = Math.Min(span.Length, readSoFar.Length - written);
593+
span.Slice(0, amountToWrite).CopyTo(readSoFar.Slice(written));
594+
written += amountToWrite;
591595

592-
span = _buffer;
596+
span = _buffer;
593597

594-
if (span.StartsWith(leftToMatch))
595-
{
596-
HasValueSequence = true;
597-
SequencePosition start = new SequencePosition(startPosition.GetObject(), startPosition.GetInteger() + startConsumed);
598-
SequencePosition end = new SequencePosition(_currentPosition.GetObject(), _currentPosition.GetInteger() + leftToMatch.Length);
599-
ValueSequence = _sequence.Slice(start, end);
600-
consumed = leftToMatch.Length;
601-
return true;
602-
}
598+
if (span.StartsWith(leftToMatch))
599+
{
600+
HasValueSequence = true;
601+
SequencePosition start = new SequencePosition(startPosition.GetObject(), startPosition.GetInteger() + startConsumed);
602+
SequencePosition end = new SequencePosition(_currentPosition.GetObject(), _currentPosition.GetInteger() + leftToMatch.Length);
603+
ValueSequence = _sequence.Slice(start, end);
604+
consumed = leftToMatch.Length;
605+
return true;
606+
}
603607

604-
if (!leftToMatch.StartsWith(span))
605-
{
606-
_bytePositionInLine += FindMismatch(span, leftToMatch);
608+
if (!leftToMatch.StartsWith(span))
609+
{
610+
_bytePositionInLine += FindMismatch(span, leftToMatch);
607611

608-
amountToWrite = AmountToWrite(span, _bytePositionInLine, readSoFar, written);
609-
span.Slice(0, amountToWrite).CopyTo(readSoFar.Slice(written));
610-
written += amountToWrite;
612+
amountToWrite = AmountToWrite(span, _bytePositionInLine, readSoFar, written);
613+
span.Slice(0, amountToWrite).CopyTo(readSoFar.Slice(written));
614+
written += amountToWrite;
611615

612-
goto Throw;
613-
}
616+
goto Throw;
617+
}
614618

615-
leftToMatch = leftToMatch.Slice(span.Length);
616-
alreadyMatched = span.Length;
619+
leftToMatch = leftToMatch.Slice(span.Length);
620+
alreadyMatched = span.Length;
621+
}
617622
}
618-
}
619623

620-
static int AmountToWrite(ReadOnlySpan<byte> span, long bytePositionInLine, ReadOnlySpan<byte> readSoFar, int written)
621-
{
622-
return Math.Min(
623-
readSoFar.Length - written,
624-
Math.Min(span.Length, (int)bytePositionInLine + 1));
624+
static int AmountToWrite(ReadOnlySpan<byte> span, long bytePositionInLine, ReadOnlySpan<byte> readSoFar, int written)
625+
{
626+
return Math.Min(
627+
readSoFar.Length - written,
628+
Math.Min(span.Length, (int)bytePositionInLine + 1));
629+
}
630+
Throw:
631+
_totalConsumed = prevTotalConsumed;
632+
consumed = default;
633+
_currentPosition = copy;
634+
throw GetInvalidLiteralMultiSegment(readSoFar.Slice(0, written).ToArray());
625635
}
626-
Throw:
627-
_totalConsumed = prevTotalConsumed;
628-
consumed = default;
629-
_currentPosition = copy;
630-
throw GetInvalidLiteralMultiSegment(readSoFar.Slice(0, written).ToArray());
631636
}
632637

633638
private static int FindMismatch(ReadOnlySpan<byte> span, ReadOnlySpan<byte> literal)

0 commit comments

Comments
 (0)