File: ApiLifecycle\Json\JsonReader.cs
Web Access
Project: src\src\Analyzers\Microsoft.Analyzers.Local\Microsoft.Analyzers.Local.csproj (Microsoft.Analyzers.Local)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// Forked from StyleCop.Analyzers repo.
 
using System;
using System.Globalization;
using System.IO;
using System.Text;
 
namespace Microsoft.Extensions.LocalAnalyzers.Json;
 
/// <summary>
/// Represents a reader that can read JsonValues.
/// </summary>
internal sealed class JsonReader
{
    private readonly TextScanner _scanner;
 
    private JsonReader(TextReader reader)
    {
        _scanner = new TextScanner(reader);
    }
 
    /// <summary>
    /// Creates a JsonValue by using the given TextReader.
    /// </summary>
    /// <param name="reader">The TextReader used to read a JSON message.</param>
    /// <returns>The parsed <see cref="JsonValue"/>.</returns>
    public static JsonValue Parse(TextReader reader)
    {
        if (reader == null)
        {
            throw new ArgumentNullException(nameof(reader));
        }
 
        return new JsonReader(reader).Parse();
    }
 
    /// <summary>
    /// Creates a JsonValue by reader the JSON message in the given string.
    /// </summary>
    /// <param name="source">The string containing the JSON message.</param>
    /// <returns>The parsed <see cref="JsonValue"/>.</returns>
    public static JsonValue Parse(string source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
 
        using var reader = new StringReader(source);
 
        return Parse(reader);
    }
 
    private string ReadJsonKey()
    {
        return ReadString();
    }
 
    private JsonValue ReadJsonValue()
    {
        _scanner.SkipWhitespace();
 
        var next = _scanner.Peek();
 
        if (char.IsNumber(next))
        {
            return ReadNumber();
        }
 
        return next switch
        {
            '{' => ReadObject(),
            '[' => ReadArray(),
            '"' => ReadString(),
            '-' => ReadNumber(),
            't' or 'f' => ReadBoolean(),
            'n' => ReadNull(),
            _ => throw new JsonParseException(
                            ParsingError.InvalidOrUnexpectedCharacter,
                            _scanner.Position),
        };
    }
 
    private JsonValue ReadNull()
    {
        _scanner.Assert("null");
        return JsonValue.Null;
    }
 
    private JsonValue ReadBoolean()
    {
        switch (_scanner.Peek())
        {
            case 't':
                _scanner.Assert("true");
                return true;
 
            default:
                _scanner.Assert("false");
                return false;
        }
    }
 
    private void ReadDigits(StringBuilder builder)
    {
        while (true)
        {
            int next = _scanner.Peek(throwAtEndOfFile: false);
            if (next == -1 || !char.IsNumber((char)next))
            {
                return;
            }
 
            _ = builder.Append(_scanner.Read());
        }
    }
 
    private JsonValue ReadNumber()
    {
        var builder = new StringBuilder();
 
        if (_scanner.Peek() == '-')
        {
            _ = builder.Append(_scanner.Read());
        }
 
        if (_scanner.Peek() == '0')
        {
            _ = builder.Append(_scanner.Read());
        }
        else
        {
            ReadDigits(builder);
        }
 
        if (_scanner.Peek(throwAtEndOfFile: false) == '.')
        {
            _ = builder.Append(_scanner.Read());
 
            ReadDigits(builder);
        }
 
        if (_scanner.Peek(throwAtEndOfFile: false) == 'e' || _scanner.Peek(throwAtEndOfFile: false) == 'E')
        {
            _ = builder.Append(_scanner.Read());
 
            var next = _scanner.Peek();
 
            switch (next)
            {
                case '+':
                case '-':
                    _ = builder.Append(_scanner.Read());
                    break;
            }
 
            ReadDigits(builder);
        }
 
        return double.Parse(
            builder.ToString(),
            CultureInfo.InvariantCulture);
    }
 
    private string ReadString()
    {
        var builder = new StringBuilder();
 
        _scanner.Assert('"');
 
        while (true)
        {
            var errorPosition = _scanner.Position;
            var c = _scanner.Read();
 
            if (c == '\\')
            {
                errorPosition = _scanner.Position;
                c = _scanner.Read();
 
                _ = char.ToLowerInvariant(c) switch
                {
                    '"' or '\\' or '/' => builder.Append(c),
                    'b' => builder.Append('\b'),
                    'f' => builder.Append('\f'),
                    'n' => builder.Append('\n'),
                    'r' => builder.Append('\r'),
                    't' => builder.Append('\t'),
                    'u' => builder.Append(ReadUnicodeLiteral()),
                    _ => throw new JsonParseException(
                                                ParsingError.InvalidOrUnexpectedCharacter,
                                                errorPosition),
                };
            }
            else if (c == '"')
            {
                break;
            }
            else
            {
                if (char.IsControl(c))
                {
                    throw new JsonParseException(
                        ParsingError.InvalidOrUnexpectedCharacter,
                        errorPosition);
                }
 
                _ = builder.Append(c);
            }
        }
 
        return builder.ToString();
    }
 
    private int ReadHexDigit()
    {
        var errorPosition = _scanner.Position;
#pragma warning disable S109 // Magic numbers should not be used
        return char.ToUpperInvariant(_scanner.Read()) switch
        {
            '0' => 0,
            '1' => 1,
            '2' => 2,
            '3' => 3,
            '4' => 4,
            '5' => 5,
            '6' => 6,
            '7' => 7,
            '8' => 8,
            '9' => 9,
            'A' => 10,
            'B' => 11,
            'C' => 12,
            'D' => 13,
            'E' => 14,
            'F' => 15,
            _ => throw new JsonParseException(
                            ParsingError.InvalidOrUnexpectedCharacter,
                            errorPosition),
        };
    }
 
    private char ReadUnicodeLiteral()
    {
        int value = 0;
 
        value += ReadHexDigit() * 4096; // 16^3
        value += ReadHexDigit() * 256;  // 16^2
        value += ReadHexDigit() * 16;   // 16^1
        value += ReadHexDigit();        // 16^0
 
        return (char)value;
    }
#pragma warning restore S109 // Magic numbers should not be used
    private JsonObject ReadObject()
    {
        return ReadObject([]);
    }
 
    private JsonObject ReadObject(JsonObject jsonObject)
    {
        _scanner.Assert('{');
 
        _scanner.SkipWhitespace();
 
        if (_scanner.Peek() == '}')
        {
            _ = _scanner.Read();
        }
        else
        {
            while (true)
            {
                _scanner.SkipWhitespace();
 
                var errorPosition = _scanner.Position;
                var key = ReadJsonKey();
 
                if (jsonObject.ContainsKey(key))
                {
                    throw new JsonParseException(
                        ParsingError.DuplicateObjectKeys,
                        errorPosition);
                }
 
                _scanner.SkipWhitespace();
 
                _scanner.Assert(':');
 
                _scanner.SkipWhitespace();
 
                var value = ReadJsonValue();
 
                _ = jsonObject.Add(key, value);
 
                _scanner.SkipWhitespace();
 
                errorPosition = _scanner.Position;
                var next = _scanner.Read();
                if (next == ',')
                {
                    // Allow trailing commas in objects
                    _scanner.SkipWhitespace();
                    if (_scanner.Peek() == '}')
                    {
                        next = _scanner.Read();
                    }
                }
 
                if (next == '}')
                {
                    break;
                }
                else if (next != ',')
                {
                    throw new JsonParseException(
                     ParsingError.InvalidOrUnexpectedCharacter,
                     errorPosition);
                }
            }
        }
 
        return jsonObject;
    }
 
    private JsonArray ReadArray()
    {
        return ReadArray([]);
    }
 
    private JsonArray ReadArray(JsonArray jsonArray)
    {
        _scanner.Assert('[');
 
        _scanner.SkipWhitespace();
 
        if (_scanner.Peek() == ']')
        {
            _ = _scanner.Read();
        }
        else
        {
            while (true)
            {
                _scanner.SkipWhitespace();
 
                var value = ReadJsonValue();
 
                _ = jsonArray.Add(value);
 
                _scanner.SkipWhitespace();
 
                var errorPosition = _scanner.Position;
                var next = _scanner.Read();
                if (next == ',')
                {
                    // Allow trailing commas in arrays
                    _scanner.SkipWhitespace();
                    if (_scanner.Peek() == ']')
                    {
                        next = _scanner.Read();
                    }
                }
 
                if (next == ']')
                {
                    break;
                }
                else if (next != ',')
                {
                    throw new JsonParseException(
                        ParsingError.InvalidOrUnexpectedCharacter,
                        errorPosition);
                }
            }
        }
 
        return jsonArray;
    }
 
    private JsonValue Parse()
    {
        _scanner.SkipWhitespace();
        return ReadJsonValue();
    }
}