• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2021-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16package escompat
17
18export class JSON {
19    /**
20     * Converts byte to JSON format
21     *
22     * @param d: byte - byte to be converted to a JSON as a Number
23     *
24     * @returns String - JSON representation of byte
25     */
26    public static stringify(d: byte): String {
27        return StringBuilder.toString(d)
28    }
29
30    /**
31     * Converts short to JSON format
32     *
33     * @param d: short - short to be converted to a JSON as a Number
34     *
35     * @returns String - JSON representation of short
36     */
37    public static stringify(d: short): String {
38        return StringBuilder.toString(d)
39    }
40
41    /**
42     * Converts int to JSON format
43     *
44     * @param d: int - int to be converted to a JSON as a Number
45     *
46     * @returns String - JSON representation of int
47     */
48    public static stringify(d: int): String {
49        return StringBuilder.toString(d)
50    }
51
52    /**
53     * Converts long to JSON format
54     *
55     * @param d: long - long to be converted to a JSON as a Number
56     *
57     * @returns String - JSON representation of long
58     */
59    public static stringify(d: long): String {
60        return StringBuilder.toString(d)
61    }
62
63    /**
64     * Converts float to JSON format
65     *
66     * @param d: float - float to be converted to a JSON as a Number
67     *
68     * @returns String - JSON representation of float
69     */
70    public static stringify(d: float): String {
71        if (Float.isFinite(d)) {
72            return StringBuilder.toString(d)
73        } else {
74            return "null"
75        }
76    }
77
78    /**
79     * Converts double to JSON format
80     *
81     * @param d: double - double to be converted to a JSON as a Number
82     *
83     * @returns String - JSON representation of double
84     */
85    public static stringify(d: double): String {
86        if (Double.isFinite(d)) {
87            return StringBuilder.toString(d)
88        } else {
89            return "null"
90        }
91    }
92
93    /**
94     * Converts char to JSON format
95     *
96     * @param d: char - char to be converted to a JSON as a String
97     *
98     * @returns String - JSON representation of char
99     */
100    public static stringify(d: char): String {
101        return "\"" + StringBuilder.toString(d) + "\""
102    }
103
104    /**
105     * Converts boolean to JSON format
106     *
107     * @param d: boolean - boolean to be converted to a JSON as a Boolean literal
108     *
109     * @returns String - JSON representation of boolean
110     */
111    public static stringify(d: boolean): String {
112        if (d) {
113            return "true"
114        }
115        return "false"
116    }
117
118    private static readonly ESCAPED_CHARS: char[] = [c'\"', c'\\', c'\/', c'\b', c'\f', c'\n', c'\r', c'\t']
119    private static readonly NON_ESCAPED_CHARS: char[] = [c'"', c'\\', c'/', c'b', c'f', c'n', c'r', c't']
120    private static readonly JSON_SPACE_INDENT_LIMIT = 10
121
122    /**
123     * Converts String to JSON format
124     *
125     * @param d: String - byte to be converted to a JSON as a String
126     *
127     * @returns String - JSON representation of byte
128     */
129    public static stringify(d: String): String {
130        let sb = new StringBuilder([c'\"'])
131        let len = d.getLength()
132
133        let prevChar: char = 0
134        for (let i = 0; i < len; ++i) {
135            let currChar = d.charAt(i)
136
137            if (Char.isHighSurrogate(currChar)) {
138                if (i != 0 && Char.isHighSurrogate(prevChar)) {
139                    JSON.appendCharHex(sb, prevChar)
140                }
141            } else if (Char.isLowSurrogate(currChar)) {
142                if (i != 0 && Char.isHighSurrogate(prevChar)) {
143                    sb.append(prevChar)
144                    sb.append(currChar)
145                } else {
146                    JSON.appendCharHex(sb, currChar)
147                }
148            } else {
149                if (i != 0 && Char.isHighSurrogate(prevChar)) {
150                    JSON.appendCharHex(sb, prevChar)
151                }
152
153                let escapedCharIndex = lastIndexOf(JSON.ESCAPED_CHARS, currChar, JSON.ESCAPED_CHARS.length)
154                if (escapedCharIndex != -1) {
155                    sb.append(c'\\')
156                    sb.append(JSON.NON_ESCAPED_CHARS[escapedCharIndex])
157                } else {
158                    sb.append(currChar)
159                }
160            }
161
162            prevChar = currChar
163        }
164
165        if (len > 0) {
166            if (Char.isHighSurrogate(prevChar)) {
167                JSON.appendCharHex(sb, prevChar)
168            }
169        }
170
171        sb.append(c'\"')
172        return sb.toString()
173    }
174
175    private static appendCharHex(buffer: StringBuilder, chr: char): StringBuilder {
176        buffer.append("\\u").append(new Int(chr).toString(16))
177        return buffer
178    }
179
180    /**
181     * Converts an object to a JavaScript Object Notation (JSON) string.
182     *
183     * @param obj: Object An object to be converted.
184     *
185     * @returns String - JSON representation of Object
186     */
187    public static stringify(obj: NullishType): String {
188        return new JSONWriter().write(obj)
189    }
190
191    /**
192     * Converts an object to a JavaScript Object Notation (JSON) string.
193     *
194     * @param obj: Object An object to be converted.
195     *
196     * @param replacer A function that transforms the results.
197     *
198     * @param space A string or number that's used to insert white space
199     * (including indentation, line break characters, etc.) into the output JSON string for readability purposes.
200     *
201     * @returns String - JSON representation of Object
202     */
203    public static stringify(
204                obj: NullishType,
205                replacer: ((key: string, value: NullishType) => NullishType) | undefined | null,
206                space?: string | number): string {
207
208        const replacerOpt: ((key: string, value: NullishType) => NullishType) | undefined = (replacer === null) ? undefined : replacer
209
210        if (space instanceof String || space === undefined) {
211            return new JSONWriter(replacerOpt, space).write(obj)
212        } else {
213            return new JSONWriter(replacerOpt, JSON.spaceFromNumber(space as number)).write(obj)
214        }
215    }
216
217    /**
218     * Converts an object to a JavaScript Object Notation (JSON) string.
219     *
220     * @param obj: Object An object to be converted.
221     *
222     * @param replacer An array with elements indicating names of the properties in the object
223     * that should be included in the resulting JSON string
224     *
225     * @param space A string or number that's used to insert white space
226     * (including indentation, line break characters, etc.) into the output JSON string for readability purposes.
227     *
228     * @returns String - JSON representation of Object
229     */
230    public static stringify(obj: NullishType, replacer: (string | number)[], space?: string | number): string {
231        const filter = new string[replacer.length]
232        for (let i = 0; i < replacer.length; i++) {
233            filter[i] = `${replacer[i]}`
234        }
235
236        if (space instanceof String || space === undefined) {
237            return new JSONWriter(filter, space).write(obj)
238        } else {
239            return new JSONWriter(filter, JSON.spaceFromNumber(space as number)).write(obj)
240        }
241    }
242
243    private static spaceFromNumber(space: number): String {
244        let spacesCount = 0
245        if (space > 0) {
246            if (space > JSON.JSON_SPACE_INDENT_LIMIT) {
247                spacesCount = JSON.JSON_SPACE_INDENT_LIMIT
248            } else {
249                spacesCount = space as int
250            }
251        }
252
253        return " ".repeat(spacesCount)
254    }
255
256    public static native stringify(d: JSValue): String;
257
258    //--------------------------
259    //          arrays
260    //--------------------------
261
262    /**
263     * Converts bytes array to JSON format
264     *
265     * @param d: byte[] - bytes array to be converted to a JSON as an Array of Numbers
266     *
267     * @returns String - JSON representation of bytes array
268     */
269    public static stringify(d: byte[]): String {
270        let s = new StringBuilder("[")
271        let last_elem = d.length - 1
272        for (let i = 0; i < last_elem; ++i) {
273            s.append(d[i])
274            s.append(',')
275        }
276        if (d.length > 0) {
277            s.append(d[last_elem])
278        }
279        s.append(']')
280        return s.toString()
281    }
282
283    /**
284     * Converts shorts array to JSON format
285     *
286     * @param d: short[] - shorts array to be converted to a JSON as an Array of Numbers
287     *
288     * @returns String - JSON representation of shorts array
289     */
290    public static stringify(d: short[]): String {
291        let s = new StringBuilder("[")
292        let last_elem = d.length - 1
293        for (let i = 0; i < last_elem; ++i) {
294            s.append(d[i])
295            s.append(',')
296        }
297        if (d.length > 0) {
298            s.append(d[last_elem])
299        }
300        s.append(']')
301        return s.toString()
302    }
303
304    /**
305     * Converts ints array to JSON format
306     *
307     * @param d: int[] - ints array to be converted to a JSON as an Array of Numbers
308     *
309     * @returns String - JSON representation of ints array
310     */
311    public static stringify(d: int[]): String {
312        let s = new StringBuilder("[")
313        let last_elem = d.length - 1
314        for (let i = 0; i < last_elem; ++i) {
315            s.append(d[i])
316            s.append(',')
317        }
318        if (d.length > 0) {
319            s.append(d[last_elem])
320        }
321        s.append(']')
322        return s.toString()
323    }
324
325    /**
326     * Converts longs array to JSON format
327     *
328     * @param d: long[] - longs array to be converted to a JSON as an Array of Numbers
329     *
330     * @returns String - JSON representation of longs array
331     */
332    public static stringify(d: long[]): String {
333        let s = new StringBuilder("[")
334        let last_elem = d.length - 1
335        for (let i = 0; i < last_elem; ++i) {
336            s.append(d[i])
337            s.append(',')
338        }
339        if (d.length > 0) {
340            s.append(d[last_elem])
341        }
342        s.append(']')
343        return s.toString()
344    }
345
346    /**
347     * Converts array of bytes to JSON format
348     *
349     * @param d: byte[] - array of byte to be converted to a JSON as an Array of Numbers
350     *
351     * @returns String - JSON representation of array of bytes
352     */
353    public static stringify(d: float[]): String {
354        let s = new StringBuilder("[")
355        let last_elem = d.length - 1
356        for (let i = 0; i < last_elem; ++i) {
357            s.append(JSON.stringify(d[i]))
358            s.append(',')
359        }
360        if (d.length > 0) {
361            s.append(JSON.stringify(d[last_elem]))
362        }
363        s.append(']')
364        return s.toString()
365    }
366
367    /**
368     * Converts doubles array to JSON format
369     *
370     * @param d: double[] - doubles array to be converted to a JSON as an Array of Numbers
371     *
372     * @returns String - JSON representation of doubles array
373     */
374    public static stringify(d: double[]): String {
375        let s = new StringBuilder("[")
376        let last_elem = d.length - 1
377        for (let i = 0; i < last_elem; ++i) {
378            s.append(JSON.stringify(d[i]))
379            s.append(',')
380        }
381        if (d.length > 0) {
382            s.append(JSON.stringify(d[last_elem]))
383        }
384        s.append(']')
385        return s.toString()
386    }
387
388    /**
389     * Converts chars array to JSON format
390     *
391     * @param d: char[] - chars array  to be converted to a JSON as an Array of Numbers
392     *
393     * @returns String - JSON representation of chars array
394     */
395    public static stringify(d: char[]): String {
396        let s = new StringBuilder("[")
397        let last_elem = d.length - 1
398        for (let i = 0; i < last_elem; ++i) {
399            s.append("\"" + d[i] + "\"")
400            s.append(',')
401        }
402        if (d.length > 0) {
403            s.append("\"" + d[last_elem] + "\"")
404        }
405        s.append(']')
406        return s.toString()
407    }
408
409    /**
410     * Converts booleans array to JSON format
411     *
412     * @param d: boolean[] - booleans array to be converted to a JSON as an Array of Boolean literals
413     *
414     * @returns String - JSON representation of booleans array
415     */
416    public static stringify(d: boolean[]): String {
417        let s = new StringBuilder("[")
418        let last_elem = d.length - 1
419        for (let i = 0; i < last_elem; ++i) {
420            if (d[i]) {
421                s.append("true,")
422            } else {
423                s.append("false,")
424            }
425        }
426        if (d.length > 0) {
427            s.append(d[last_elem])
428        }
429        s.append(']')
430        return s.toString()
431    }
432
433    private static IsBoxedType(typ : Type) : boolean {
434        return typ instanceof BooleanType
435                || typ instanceof ByteType
436                || typ instanceof CharType
437                || typ instanceof ShortType
438                || typ instanceof IntType
439                || typ instanceof LongType
440                || typ instanceof FloatType
441                || typ instanceof DoubleType
442    }
443
444    // TODO(kirill-mitkin): Map<Long, Long> blocked by internal issue
445    private static checkType(typ: Type, used: Array<TypeColor>): boolean {
446        let id = typ.getId()
447        let ind = used.findIndex((tc: TypeColor) => tc.typeID == id)
448        if (ind != -1) {
449            return (used.at(ind) as TypeColor).color == TypeColor.VISITED
450        }
451        used.push(new TypeColor(id, TypeColor.AT_STACK))
452        let ok = true
453        if (typ instanceof ClassType) {
454            let ct = typ as ClassType
455            ok &= ct.hasEmptyConstructor()
456            for (let f = 0; f < ct.getFieldsNum(); f++) {
457                ok &= JSON.checkType(ct.getField(f).getType(), used)
458            }
459        } else if (typ instanceof ArrayType) {
460            let et = (typ as ArrayType).getElementType()
461            ok = JSON.checkType(et, used)
462        } else if (typ instanceof StringType
463                    || typ instanceof DoubleType
464                    || typ instanceof BooleanType
465                    || typ instanceof NullType) {
466            ok = true
467        } else {
468            ok = false
469        }
470        (used.at(ind) as TypeColor).color = TypeColor.VISITED
471        return ok
472    }
473
474    // TODO(kirill-mitkin): For testing purpose only, shpuld be replaced by error checking
475    public static isCorrectForParsing(typ: Type): boolean {
476        return JSON.checkType(typ, new Array<TypeColor>())
477    }
478
479    public static parse<T>(text: String, type: Type): Nullish<T> {
480        return JSON.parse<T>(text, undefined, type)
481    }
482
483    public static parse<T>(text: string, reviver: ((key: string, value: NullishType) => NullishType) | undefined, type: Type): Nullish<T> {
484        if (!JSON.checkType(type, new Array<TypeColor>())) {
485            throw new Error("Incorrect type: " + type.toString())
486        }
487
488        let jsonValue = JSONParser.parse(text)
489        const parsingResult = new JSONValueParser(reviver).parse(jsonValue, type) as T
490
491        if (reviver !== undefined) {
492            return reviver("", parsingResult) as T
493        } else {
494            return parsingResult
495        }
496    }
497}
498
499class JSONWriter {
500    // NOTE(cheezzario) replace with Type.for<JSValue>() when it will be implemented
501    private static readonly STD_CORE_INTEROP_JSVALUE_TYPE = (Type.of([] as JSValue[]) as ArrayType).getElementType()
502    private static readonly ESCOMPAT_ARRAY_TYPE = Type.of(new Array<Object>(0))
503
504    private readonly replacer: ((key: String, value: NullishType) => NullishType) | undefined
505    private readonly fieldsFilter: string[]
506    private readonly space: String | undefined
507
508    private readonly useReplacer: boolean
509    private readonly useFieldsFilter: boolean
510    private readonly useIndent: boolean
511
512    private indentLevel: int = 0
513
514    private readonly path = new Set<Object>()
515    private buffer = new StringBuilder()
516
517    constructor(replacer?: (key: String, value: NullishType) => NullishType, space?: String) {
518        this.replacer = replacer
519        this.fieldsFilter = []
520        this.space = space
521
522        this.useReplacer = this.replacer != undefined
523        this.useFieldsFilter = false
524        this.useIndent = !(this.space == undefined || this.space == "")
525    }
526
527    constructor(filter: string[], space?: String) {
528        this.replacer = undefined
529        this.fieldsFilter = filter
530        this.space = space
531
532        this.useReplacer = false
533        this.useFieldsFilter = true
534        this.useIndent = !(this.space == undefined || this.space == "")
535    }
536
537    write(obj: NullishType): String {
538        if (this.useReplacer) {
539            this.writeObject(this.replacer!("", obj))
540        } else {
541            this.writeObject(obj)
542        }
543
544        return this.buffer.toString()
545    }
546
547    private writeObject(obj: NullishType): void {
548        if (obj === null) {
549            this.buffer.append("null")
550        } else if (obj === undefined) {
551            this.buffer.append("undefined")
552        } else if (obj instanceof String) {
553            this.buffer.append(JSON.stringify(obj as String))
554        } else if (this.writeValueTypeWrapper(obj)) {
555            // nothing to do - write completed successfully
556        } else {
557            const objType = Type.of(obj)
558
559            if (objType.getName() == JSONWriter.STD_CORE_INTEROP_JSVALUE_TYPE.getName()) {
560                this.buffer.append(JSON.stringify(obj as JSValue))
561            } else if (objType instanceof ArrayType) {
562                this.writeArrayValue(Value.of(obj) as ArrayValue, objType as ArrayType)
563            } else if (objType.getName() == JSONWriter.ESCOMPAT_ARRAY_TYPE.getName()) {
564                this.writeEscompatArray(obj as Array<Object>)
565            } else if (objType instanceof ClassType) {
566                this.writeClassValue(obj, objType as ClassType)
567            } else if (objType instanceof LambdaType) {
568                this.buffer.append("undefined")
569            } else {
570                assert false : "unsupported object type: " + objType
571            }
572        }
573    }
574
575    private writeClassValue(obj: Object, objType: ClassType): void {
576        const currentBuffer = this.buffer
577        this.buffer = new StringBuilder("{")
578
579        if (this.useIndent) {
580            this.buffer.append("\n")
581            this.indentLevel += 1
582        }
583
584        const writableFields = this.getWritableFields(objType, Value.of(obj) as ClassValue)
585
586        let fieldDumped = false
587        if (writableFields.length > 0) {
588            this.path.add(obj)
589            fieldDumped = this.writeClassFields(writableFields)
590            this.path.delete(obj)
591        }
592
593        if (this.useIndent) {
594            this.indentLevel -= 1
595            this.buffer.append("\n")
596        }
597
598        this.writePadding()
599        this.buffer.append("}")
600
601        if (fieldDumped) {
602            currentBuffer.append(this.buffer.toString())
603        } else {
604            currentBuffer.append("{}")
605        }
606
607        this.buffer = currentBuffer
608    }
609
610    private writeClassFields(writableFields: Array<[Field, Value]>): boolean {
611        const FIELD_TYPE = 0
612        const FIELD_VALUE = 1
613
614        let fieldDumped = false
615        for (let fieldIdx = 0; fieldIdx < writableFields.length; fieldIdx++) {
616            const fieldTypeValuePair = writableFields[fieldIdx]
617
618            const objField = fieldTypeValuePair[FIELD_TYPE]
619            const objFieldValue = fieldTypeValuePair[FIELD_VALUE]
620
621            const objToDump = this.useReplacer ? this.replacer!(objField.getName(), objFieldValue.getData()) : objFieldValue.getData()
622            if (objToDump !== undefined) {
623                if (fieldDumped) {
624                    this.buffer.append(",")
625                    if (this.useIndent) {
626                        this.buffer.append("\n")
627                    }
628                } else {
629                    fieldDumped = true
630                }
631
632                this.checkReferencesCycle(objToDump)
633                this.writePadding()
634
635                this.buffer.append('"')
636                this.buffer.append(objField.getName())
637                this.buffer.append('":')
638
639                if (this.useIndent) {
640                    this.buffer.append(" ")
641                }
642
643                this.writeObject(objToDump)
644            }
645        }
646
647        return fieldDumped
648    }
649
650    private getWritableFields(classType: ClassType, classValue: ClassValue): Array<[Field, Value]> {
651        const writableFields = new Array<[Field, Value]>()
652
653        if (this.useFieldsFilter) {
654            for (let fieldIdx = 0; fieldIdx < this.fieldsFilter.length; fieldIdx++) {
655                const fieldName = this.fieldsFilter[fieldIdx]
656                if (!classType.hasField(fieldName)) {
657                    continue
658                }
659
660                const fieldType = classType.getFieldByName(fieldName)
661                if (!this.isWritableFieldType(fieldType)) {
662                    continue
663                }
664
665                const fieldValue = classValue.getFieldByName(fieldName)
666
667                const fieldTypeValuePair: [Field, Value] = [fieldType, fieldValue]
668                writableFields.push(fieldTypeValuePair)
669            }
670        } else {
671            const fieldsCount = classValue.getFieldsNum()
672            for (let fieldIdx = 0; fieldIdx < fieldsCount; fieldIdx++) {
673                const fieldType = classType.getField(fieldIdx)
674                if (!this.isWritableFieldType(fieldType)) {
675                    continue
676                }
677
678                const fieldValue = classValue.getField(fieldIdx)
679
680                const fieldTypeValuePair: [Field, Value] = [fieldType, fieldValue]
681                writableFields.push(fieldTypeValuePair)
682            }
683        }
684
685        return writableFields
686    }
687
688    private isWritableFieldType(field: Field): boolean {
689        const fieldType = field.getType()
690
691        if (field.isStatic() || fieldType instanceof LambdaType) {
692            return false
693        }
694
695        return true
696    }
697
698    private writeArrayValue(arrayValue: ArrayValue, arrayType: ArrayType): void {
699        this.buffer.append("[")
700
701        if (this.useIndent) {
702            this.buffer.append("\n")
703            this.indentLevel += 1
704        }
705
706        const arrayLength = arrayValue.getLength() as int
707        if (arrayLength > 0) {
708            const array: Object = arrayValue.getData()!
709
710            this.path.add(array)
711
712            const lastElementIndex = arrayLength - 1
713
714            for (let idx = 0; idx < lastElementIndex; idx++) {
715                this.writeArrayElementValue(arrayValue.getElement(idx), idx)
716                this.buffer.append(",")
717
718                if (this.useIndent) {
719                    this.buffer.append("\n")
720                }
721            }
722
723            this.writeArrayElementValue(arrayValue.getElement(lastElementIndex), lastElementIndex)
724
725            this.path.delete(array)
726        }
727
728        if (this.useIndent) {
729            this.indentLevel -= 1
730            this.buffer.append("\n")
731        }
732
733        this.writePadding()
734
735        this.buffer.append("]")
736    }
737
738    private writeArrayElementValue(elemValue: Value, elemIndex: int): void {
739        this.writeObjectsArrayElement(elemValue.getData(), elemIndex)
740    }
741
742    private writeValueTypeWrapper(obj: Object): boolean {
743        if (obj instanceof Boolean) {
744            this.buffer.append(JSON.stringify(obj.unboxed()))
745            return true
746        } else if (obj instanceof Byte) {
747            this.buffer.append(JSON.stringify(obj.unboxed()))
748            return true
749        } else if (obj instanceof Char) {
750            this.buffer.append(JSON.stringify(obj.unboxed()))
751            return true
752        } else if (obj instanceof Short) {
753            this.buffer.append(JSON.stringify(obj.unboxed()))
754            return true
755        } else if (obj instanceof Int) {
756            this.buffer.append(JSON.stringify(obj.unboxed()))
757            return true
758        } else if (obj instanceof Long) {
759            this.buffer.append(JSON.stringify(obj.unboxed()))
760            return true
761        } else if (obj instanceof Float) {
762            this.buffer.append(JSON.stringify(obj.unboxed()))
763            return true
764        } else if (obj instanceof Double) {
765            this.buffer.append(JSON.stringify(obj.unboxed()))
766            return true
767        } else {
768            return false
769        }
770    }
771
772    private writeEscompatArray<T>(array: Array<T>): void {
773        this.buffer.append("[")
774
775        if (this.useIndent) {
776            this.buffer.append("\n")
777            this.indentLevel += 1
778        }
779
780        if (array.length > 0) {
781            this.path.add(array)
782
783            const lastElementIndex = (array.length as int) - 1
784
785            for (let idx = 0; idx < lastElementIndex; idx++) {
786                this.writeObjectsArrayElement(array[idx], idx)
787                this.buffer.append(",")
788
789                if (this.useIndent) {
790                    this.buffer.append("\n")
791                }
792            }
793
794            this.writeObjectsArrayElement(array[lastElementIndex], lastElementIndex)
795
796            this.path.delete(array)
797        }
798
799        if (this.useIndent) {
800            this.indentLevel -= 1
801            this.buffer.append("\n")
802        }
803
804        this.writePadding()
805
806        this.buffer.append("]")
807    }
808
809    private writeObjectsArrayElement(arrayElement: NullishType, elementIndex: int): void {
810        this.writePadding()
811
812        const replacedArrayElement = this.useReplacer ? this.replacer!("" + elementIndex, arrayElement) : arrayElement
813
814        const arrayElementType = Type.of(replacedArrayElement)
815        if (replacedArrayElement == null || arrayElementType instanceof LambdaType) {
816            this.buffer.append("null")
817        } else {
818            this.checkReferencesCycle(replacedArrayElement)
819            this.writeObject(replacedArrayElement)
820        }
821    }
822
823    private checkReferencesCycle(obj: NullishType): void {
824        if (obj == null) {
825            return
826        }
827
828        const cycleDetected = this.path.has(obj)
829        if (cycleDetected) {
830            throw new TypeError("cyclic object value")
831        }
832    }
833
834    private writePadding() {
835        if (this.indentLevel > 0) {
836            const indent = this.space!.repeat(this.indentLevel)
837            this.buffer.append(indent)
838        }
839    }
840}
841
842class TypeColor {
843    readonly static VISITED : int = 1
844    readonly static AT_STACK : int = 2
845    typeID : long
846    color : int
847    constructor(typeID: long, color: int) {
848        this.typeID = typeID
849        this.color = color
850    }
851}
852
853class Position {
854    row: int = 1
855    col: int = 1
856    index: int = -1
857
858    makeCopy(): Position {
859        let c = new Position()
860        c.row = this.row
861        c.col = this.col
862        c.index = this.index
863        return c
864    }
865
866    restoreFromCopy(c: Position) {
867        this.row = c.row
868        this.col = c.col
869        this.index = c.index
870    }
871}
872
873export class JSONParser {
874    private json: String
875    private curPos: Position
876    private curChar: char
877    private escapeWhitespaces: boolean
878    // TODO(ivan-tyulyandin): replace the hardcode with proper use of Array<T>/Map<T> in JSONValue inheritors
879
880    constructor(json: String) {
881        this.json = json
882        this.escapeWhitespaces = true
883        this.curPos = new Position()
884    }
885
886    private getCurPosDescr(): String {
887        return "" + this.curPos.row + ":" + this.curPos.col
888    }
889
890    private getNextChar(): boolean {
891        do {
892            ++this.curPos.index
893            if (this.curPos.index >= this.json.getLength()) {
894                return false
895            }
896            this.curChar = this.json.charAt(this.curPos.index)
897            ++this.curPos.col
898            if (this.curChar == c'\n') {
899                ++this.curPos.row
900                this.curPos.col = 1
901            }
902        } while (this.escapeWhitespaces && (this.curChar == c' ' || this.curChar == c'\t' || this.curChar == c'\n'))
903        return true
904    }
905
906    /**
907     * Parses JSON into JSONValue
908     *
909     * @param json: String - a string with JSON
910     *
911     * @return JSONValue - JSON representation
912     *
913     * @throws SyntaxError if JSON is invalid
914     *
915     */
916    static parse(json: String): JSONValue {
917        let parser = new JSONParser(json)
918        let res: JSONValue = new JSONValue()
919        return parser.parse(res)
920    }
921
922    private parse(res: JSONValue): JSONValue {
923        // Fetch next symbol and call related parse method
924        // TODO(ivan-tyulyandin): replace with switch, does not work due to frontend bug with switch over static fields with equal names
925        this.getNextChar()
926        if (this.curChar == JSONObject.START_CHAR) {
927            let obj = new JSONObject()
928            this.parse(obj)
929            res = obj
930        } else if (this.curChar  == JSONArray.START_CHAR) {
931            let arr = new JSONArray()
932            this.parse(arr)
933            res = arr
934        } else if (this.curChar == JSONString.START_CHAR) {
935            let str = new JSONString()
936            this.parse(str)
937            res = str
938        } else if (this.curChar == JSONTrue.START_CHAR) {
939            let tr = new JSONTrue()
940            this.parse(tr)
941            res = tr
942        } else if (this.curChar == JSONFalse.START_CHAR) {
943            let fls = new JSONFalse()
944            this.parse(fls)
945            res = fls
946        } else if (this.curChar == JSONNull.START_CHAR) {
947            let nl = new JSONNull()
948            this.parse(nl)
949            res = nl
950        } else if (Char.isDecDigit(this.curChar) || this.curChar == c'-') {
951            let n = new JSONNumber()
952            this.parse(n)
953            res = n
954        } else {
955            throw new SyntaxError("Unexpected char \"" + this.curChar + "\" at " + this.getCurPosDescr())
956        }
957        return res
958    }
959
960    private parseKeyValue(res: JSONObject): JSONObject {
961        res.keys_.push(this.parse(new JSONValue()) as JSONString)
962        this.getNextChar()
963        if (this.curChar  != c':') {
964            throw new SyntaxError("Expected : \",\" at " + this.getCurPosDescr() + " got \"" + this.curChar + "\"")
965        }
966        res.values.push(this.parse(new JSONValue()))
967        return res
968    }
969
970    private parse(res: JSONObject): JSONObject {
971        let posCopy = this.curPos.makeCopy()
972        this.getNextChar()
973        if (this.curChar == JSONObject.END_CHAR) {
974            return res
975        }
976        this.curPos.restoreFromCopy(posCopy)
977
978        res = this.parseKeyValue(res)
979        // Parse JSONObject until }
980        while (this.getNextChar()) {
981            switch (this.curChar) {
982                case c',':
983                    res = this.parseKeyValue(res)
984                    break
985                // TODO(ivan-tyulyandin): replace by JSONObject.END_CHAR, frontend bug
986                case c'}':
987                    return res
988                default:
989                    throw new SyntaxError("Expected \",\" or \"" + JSONObject.END_CHAR + "\" at " + this.getCurPosDescr() + ", got \"" + this.curChar + "\"")
990            }
991        }
992    }
993
994    private parse(res: JSONArray): JSONArray {
995        let posCopy = this.curPos.makeCopy()
996        this.getNextChar()
997        if (this.curChar == JSONArray.END_CHAR) {
998            return res
999        }
1000        this.curPos.restoreFromCopy(posCopy)
1001
1002        res.values.push(this.parse(new JSONValue()))
1003        // Parse JSONArray until ]
1004        while (this.getNextChar()) {
1005            switch (this.curChar) {
1006                case c',':
1007                    res.values.push(this.parse(new JSONValue()))
1008                    break
1009                // TODO(ivan-tyulyandin): replace by JSONArray.END_CHAR, frontend bug
1010                case c']':
1011                    return res
1012                default:
1013                    throw new SyntaxError("Expected , or " + JSONArray.END_CHAR + " at " + this.getCurPosDescr() + ", got " + this.curChar)
1014            }
1015        }
1016    }
1017
1018    private parse(res: JSONNumber): JSONNumber {
1019        let number = new StringBuilder()
1020        let posCopy = this.curPos.makeCopy()
1021        do {
1022            number.append(this.curChar)
1023            posCopy = this.curPos.makeCopy()
1024        } while (this.getNextChar() && (Char.isDecDigit(this.curChar) ||
1025                                               this.curChar == c'.' ||
1026                                               this.curChar == c'e' ||
1027                                               this.curChar == c'-' ||
1028                                               this.curChar == c'+' ))
1029        res.value = Double.parseFloat(number.toString())
1030        this.curPos.restoreFromCopy(posCopy)
1031        return res
1032    }
1033
1034    private static readonly CAN_BE_ESCAPED_CHARS: char[] = [c'"', c'\\', c'/', c'b', c'f', c'n', c'r', c't']
1035    private static readonly ESCAPED_CHARS: char[] = [c'\"', c'\\', c'\/', c'\b', c'\f', c'\n', c'\r', c'\t']
1036
1037    private parse(res: JSONString): JSONString {
1038        let resBuilder: StringBuilder = new StringBuilder()
1039        let hasMetEscape: boolean = false
1040        this.escapeWhitespaces = false
1041        while (this.getNextChar() && ((this.curChar != JSONString.END_CHAR) || (this.curChar == JSONString.END_CHAR && hasMetEscape))) {
1042            if (!hasMetEscape) {
1043                if (this.curChar != c'\\') {
1044                    resBuilder.append(this.curChar)
1045                }
1046            } else {
1047                let escapedCharIndex = indexOf(JSONParser.CAN_BE_ESCAPED_CHARS, this.curChar, 0)
1048                if (escapedCharIndex != -1) {
1049                    resBuilder.append(JSONParser.ESCAPED_CHARS[escapedCharIndex])
1050                } else {
1051                    throw new SyntaxError("Bad escape sequence \\" + this.curChar + " at " + this.getCurPosDescr())
1052                    return res
1053                }
1054            }
1055            hasMetEscape = this.curChar == c'\\'
1056        }
1057        if (this.curChar == JSONString.END_CHAR) {
1058            res.value = resBuilder.toString()
1059        } else {
1060            throw new SyntaxError("Unexpected end of String at " + this.getCurPosDescr())
1061        }
1062        this.escapeWhitespaces = true
1063        return res
1064    }
1065
1066    private parse(res: JSONTrue): JSONTrue {
1067        let metTrue = this.getNextChar() && this.curChar == c'r'
1068                    && this.getNextChar() && this.curChar == c'u'
1069                    && this.getNextChar() && this.curChar == c'e'
1070        if (!metTrue) {
1071            throw new SyntaxError("Expected true at " + this.getCurPosDescr())
1072        }
1073        return res
1074    }
1075
1076    private parse(res: JSONFalse): JSONFalse {
1077        let metFalse = this.getNextChar() && this.curChar == c'a'
1078                    && this.getNextChar() && this.curChar == c'l'
1079                    && this.getNextChar() && this.curChar == c's'
1080                    && this.getNextChar() && this.curChar == c'e'
1081        if (!metFalse) {
1082            throw new SyntaxError("Expected false at " + this.getCurPosDescr())
1083        }
1084        return res
1085    }
1086
1087    private parse(res: JSONNull): JSONNull {
1088        let metNull = this.getNextChar() && this.curChar == c'u'
1089                    && this.getNextChar() && this.curChar == c'l'
1090                    && this.getNextChar() && this.curChar == c'l'
1091        if (!metNull) {
1092            throw new SyntaxError("Expected null at " + this.getCurPosDescr())
1093        }
1094        return res
1095    }
1096}
1097
1098class JSONValueParser {
1099    private reviver: ((key: string, value: NullishType) => NullishType) | undefined
1100
1101    constructor(reviver: ((key: string, value: NullishType) => NullishType) | undefined) {
1102        this.reviver = reviver
1103    }
1104
1105    parse(value: JSONValue, typ: Type): NullishType {
1106        if (value instanceof JSONObject && typ instanceof ClassType) {
1107            return this.parseObject(value as JSONObject, typ as ClassType)
1108        } else if (value instanceof JSONArray && typ instanceof ArrayType) {
1109            return this.parseArray(value as JSONArray, typ as ArrayType)
1110        } else if (value instanceof JSONNumber && typ instanceof DoubleType) {
1111            return new Double((value as JSONNumber).value)
1112        } else if (value instanceof JSONString && typ instanceof StringType) {
1113            return (value as JSONString).value
1114        } else if (value instanceof JSONTrue && typ instanceof BooleanType) {
1115            return new Boolean(true)
1116        } else if (value instanceof JSONFalse && typ instanceof BooleanType) {
1117            return new Boolean(false)
1118        } else if (value instanceof JSONNull && typ instanceof NullType) {
1119            return null
1120        } else {
1121            throw new Error(typ + " is expected, but get " + value)
1122        }
1123    }
1124
1125    private revivingClassFieldSetter(classVal: ClassValue, fieldName: string, fieldVal: NullishType): void {
1126        const revivedVal = this.reviver!(fieldName, fieldVal)
1127
1128        // setFieldByName doesn't allow null/undefined values
1129        if (revivedVal != null) {
1130            classVal.setFieldByName(fieldName, Value.of(revivedVal))
1131        }
1132    }
1133
1134    private classFieldSetter(classVal: ClassValue, fieldName: string, fieldVal: NullishType): void {
1135        classVal.setFieldByName(fieldName, Value.of(fieldVal))
1136    }
1137
1138    private parseObject(jsonObj: JSONObject, classType: ClassType): Object {
1139        let setClassFieldFn = (cv: ClassValue, fn: string, fv: NullishType) => { cv.setFieldByName(fn, Value.of(fv)) }
1140
1141        if (this.reviver != undefined) {
1142            setClassFieldFn = this.revivingClassFieldSetter
1143        }
1144
1145        let object = classType.make()
1146        let classVal = Value.of(object) as ClassValue
1147        const jsonObjFields = jsonObj.getFields()
1148        for (let field_num = 0; field_num < classType.getFieldsNum(); field_num++) {
1149            let classField = classType.getField(field_num)
1150            if (classField.isStatic()) {
1151                continue
1152            }
1153
1154            const jsonFieldVal = jsonObjFields.get(classField.getName())
1155            if (jsonFieldVal === undefined) {
1156                throw new Error("Cannot find " + classField.getName() + " in keys of " + classType.getName())
1157            }
1158
1159            const classFieldVal = this.parse(jsonFieldVal, classField.getType())
1160            setClassFieldFn(classVal, classField.getName(), classFieldVal)
1161        }
1162
1163        return object
1164    }
1165
1166    private revivingArrayElementSetter(array: NullishType[], index: int, elemVal: NullishType): void {
1167        const revivedVal = this.reviver!(`${index}`, elemVal)
1168        array[index] = revivedVal !== undefined ? revivedVal : null
1169    }
1170
1171    private parseArray(jVal: JSONArray, aType: ArrayType): Object | null {
1172        let len = jVal.values.length
1173        let arr = aType.make(len as int) as NullishType[]
1174
1175        let setArrayElementFn = (a: NullishType[], i: int, v: NullishType) => { a[i] = v }
1176
1177        if (this.reviver !== undefined) {
1178            setArrayElementFn = this.revivingArrayElementSetter
1179        }
1180
1181        for (let i = 0; i < len; i++) {
1182            let elem = this.parse(jVal.values.at(i) as JSONValue, aType.getElementType())
1183            setArrayElementFn(arr, i, elem)
1184        }
1185        return arr
1186    }
1187}
1188
1189export class JSONValue extends Object {}
1190
1191export class JSONObject extends JSONValue {
1192    keys_: Array<JSONString> = new Array<JSONString>()
1193    values: Array<JSONValue> = new Array<JSONValue>()
1194    readonly static START_CHAR = c'{'
1195    readonly static END_CHAR = c'}'
1196    readonly static SEPARATOR = c':'
1197    readonly static DELIMETER = c','
1198
1199    getFields(): Map<String, JSONValue> {
1200        const propsCount = this.keys_.length
1201
1202        const result = new Map<String, JSONValue>()
1203
1204        for (let i = 0; i < propsCount; i++) {
1205            const key = this.keys_[i]
1206            const val = this.values[i]
1207
1208            result.set(key.value, val)
1209        }
1210
1211        return result
1212    }
1213
1214    public override toString(): String {
1215        let res = new StringBuilder([JSONObject.START_CHAR])
1216        for (let i = 0; i < this.keys_.length - 1; ++i) {
1217            res.append("" + this.keys_.at(i) + JSONObject.SEPARATOR + this.values.at(i) + JSONObject.DELIMETER)
1218        }
1219        if (this.keys_.length > 0) {
1220            res.append("" + this.keys_.at(this.keys_.length - 1) + JSONObject.SEPARATOR + this.values.at(this.keys_.length - 1))
1221        }
1222        res.append(JSONObject.END_CHAR)
1223        return res.toString()
1224    }
1225}
1226
1227export class JSONArray extends JSONValue {
1228    values: Array<JSONValue> = new Array<JSONValue>()
1229    readonly static START_CHAR = c'['
1230    readonly static END_CHAR = c']'
1231    readonly static SEPARATOR = c','
1232
1233    public override toString(): String {
1234        let res = new StringBuilder([JSONArray.START_CHAR])
1235        for (let i = 0; i < this.values.length - 1; ++i) {
1236            res.append("" + this.values.at(i) + JSONArray.SEPARATOR)
1237        }
1238        if (this.values.length > 0) {
1239            res.append("" + this.values.at(this.values.length - 1))
1240        }
1241        res.append(JSONArray.END_CHAR)
1242        return res.toString()
1243    }
1244}
1245
1246export class JSONNumber extends JSONValue {
1247    value: double
1248
1249    public override toString(): String {
1250        return (new Double(this.value)).toString()
1251    }
1252}
1253
1254export class JSONString extends JSONValue {
1255    value: String
1256    readonly static START_CHAR = c'"'
1257    readonly static END_CHAR = c'"'
1258
1259    public override toString(): String {
1260        return this.value
1261    }
1262}
1263
1264export class JSONTrue extends JSONValue {
1265    readonly static value = "true"
1266    readonly static START_CHAR = c't'
1267
1268    public override toString(): String {
1269        return JSONTrue.value
1270    }
1271}
1272
1273export class JSONFalse extends JSONValue {
1274    readonly static value = "false"
1275    readonly static START_CHAR = c'f'
1276
1277    public override toString(): String {
1278        return JSONFalse.value
1279    }
1280}
1281
1282export class JSONNull extends JSONValue {
1283    readonly static value = "null"
1284    readonly static START_CHAR = c'n'
1285
1286    public override toString(): String {
1287        return JSONNull.value
1288    }
1289}
1290
1291export interface JSONable<T> {
1292    static createFromJSONValue(json: JSONValue): T {
1293        throw new JSONTypeError("createFromJSONValue was not overrided")
1294    }
1295}
1296