Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsonc): annotate return types #3327

Merged
merged 5 commits into from
Apr 24, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 62 additions & 68 deletions jsonc/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

import { assert } from "../_util/asserts.ts";

import type { JsonValue } from "../json/common.ts";
export type { JsonValue } from "../json/common.ts";

export interface ParseOptions {
/** Allow trailing commas at the end of arrays and objects.
*
Expand Down Expand Up @@ -41,46 +44,37 @@ export interface ParseOptions {
export function parse(
text: string,
{ allowTrailingComma = true }: ParseOptions = {},
) {
): JsonValue {
if (new.target) {
throw new TypeError("parse is not a constructor");
}
return new JSONCParser(text, { allowTrailingComma }).parse() as JSONValue;
return new JSONCParser(text, { allowTrailingComma }).parse();
}

/** Valid types as a result of JSON parsing */
export type JSONValue =
| { [key: string]: JSONValue | undefined }
| JSONValue[]
| string
| number
| boolean
| null;

enum tokenType {
beginObject,
endObject,
beginArray,
endArray,
nameSeparator,
valueSeparator,
nullOrTrueOrFalseOrNumber,
string,
enum TokenType {
BeginObject,
EndObject,
BeginArray,
EndArray,
NameSeparator,
ValueSeparator,
NullOrTrueOrFalseOrNumber,
String,
}

type Token = {
type: Exclude<
tokenType,
tokenType.string | tokenType.nullOrTrueOrFalseOrNumber
TokenType,
TokenType.String | TokenType.NullOrTrueOrFalseOrNumber
>;
sourceText?: undefined;
position: number;
} | {
type: tokenType.string;
type: TokenType.String;
sourceText: string;
position: number;
} | {
type: tokenType.nullOrTrueOrFalseOrNumber;
type: TokenType.NullOrTrueOrFalseOrNumber;
sourceText: string;
position: number;
};
Expand All @@ -101,9 +95,9 @@ class JSONCParser {
this.#tokenized = this.#tokenize();
this.#options = options;
}
parse() {
parse(): JsonValue {
const token = this.#getNext();
const res = this.#parseJSONValue(token);
const res = this.#parseJsonValue(token);

// make sure all characters have been read
const { done, value } = this.#tokenized.next();
Expand All @@ -114,7 +108,7 @@ class JSONCParser {
return res;
}
/** Read the next token. If the token is read to the end, it throws a SyntaxError. */
#getNext() {
#getNext(): Token {
const { done, value } = this.#tokenized.next();
if (done) {
throw new SyntaxError("Unexpected end of JSONC input");
Expand Down Expand Up @@ -159,22 +153,22 @@ class JSONCParser {

switch (this.#text[i]) {
case "{":
yield { type: tokenType.beginObject, position: i } as const;
yield { type: TokenType.BeginObject, position: i };
break;
case "}":
yield { type: tokenType.endObject, position: i } as const;
yield { type: TokenType.EndObject, position: i };
break;
case "[":
yield { type: tokenType.beginArray, position: i } as const;
yield { type: TokenType.BeginArray, position: i };
break;
case "]":
yield { type: tokenType.endArray, position: i } as const;
yield { type: TokenType.EndArray, position: i };
break;
case ":":
yield { type: tokenType.nameSeparator, position: i } as const;
yield { type: TokenType.NameSeparator, position: i };
break;
case ",":
yield { type: tokenType.valueSeparator, position: i } as const;
yield { type: TokenType.ValueSeparator, position: i };
break;
case '"': { // parse string token
const startIndex = i;
Expand All @@ -192,10 +186,10 @@ class JSONCParser {
shouldEscapeNext = this.#text[i] === "\\" && !shouldEscapeNext;
}
yield {
type: tokenType.string,
type: TokenType.String,
sourceText: this.#text.substring(startIndex, i + 1),
position: startIndex,
} as const;
};
break;
}
default: { // parse null, true, false or number token
Expand All @@ -207,30 +201,30 @@ class JSONCParser {
}
i--;
yield {
type: tokenType.nullOrTrueOrFalseOrNumber,
type: TokenType.NullOrTrueOrFalseOrNumber,
sourceText: this.#text.substring(startIndex, i + 1),
position: startIndex,
} as const;
};
}
}
}
}
#parseJSONValue(value: Token) {
#parseJsonValue(value: Token): JsonValue {
switch (value.type) {
case tokenType.beginObject:
case TokenType.BeginObject:
return this.#parseObject();
case tokenType.beginArray:
case TokenType.BeginArray:
return this.#parseArray();
case tokenType.nullOrTrueOrFalseOrNumber:
case TokenType.NullOrTrueOrFalseOrNumber:
return this.#parseNullOrTrueOrFalseOrNumber(value);
case tokenType.string:
case TokenType.String:
return this.#parseString(value);
default:
throw new SyntaxError(buildErrorMessage(value));
}
}
#parseObject() {
const target: Record<string, unknown> = {};
#parseObject(): { [key: string]: JsonValue | undefined } {
const target: { [key: string]: JsonValue | undefined } = {};
// ┌─token1
// { }
// ┌─────────────token1
Expand All @@ -257,39 +251,39 @@ class JSONCParser {
const token1 = this.#getNext();
if (
(isFirst || this.#options.allowTrailingComma) &&
token1.type === tokenType.endObject
token1.type === TokenType.EndObject
) {
return target;
}
if (token1.type !== tokenType.string) {
if (token1.type !== TokenType.String) {
throw new SyntaxError(buildErrorMessage(token1));
}
const key = this.#parseString(token1);

const token2 = this.#getNext();
if (token2.type !== tokenType.nameSeparator) {
if (token2.type !== TokenType.NameSeparator) {
throw new SyntaxError(buildErrorMessage(token2));
}

const token3 = this.#getNext();
Object.defineProperty(target, key, {
value: this.#parseJSONValue(token3),
value: this.#parseJsonValue(token3),
writable: true,
enumerable: true,
configurable: true,
});

const token4 = this.#getNext();
if (token4.type === tokenType.endObject) {
if (token4.type === TokenType.EndObject) {
return target;
}
if (token4.type !== tokenType.valueSeparator) {
if (token4.type !== TokenType.ValueSeparator) {
throw new SyntaxError(buildErrorMessage(token4));
}
}
}
#parseArray() {
const target: unknown[] = [];
#parseArray(): JsonValue[] {
const target: JsonValue[] = [];
// ┌─token1
// [ ]
// ┌─────────────token1
Expand All @@ -308,23 +302,23 @@ class JSONCParser {
const token1 = this.#getNext();
if (
(isFirst || this.#options.allowTrailingComma) &&
token1.type === tokenType.endArray
token1.type === TokenType.EndArray
) {
return target;
}
target.push(this.#parseJSONValue(token1));
target.push(this.#parseJsonValue(token1));

const token2 = this.#getNext();
if (token2.type === tokenType.endArray) {
if (token2.type === TokenType.EndArray) {
return target;
}
if (token2.type !== tokenType.valueSeparator) {
if (token2.type !== TokenType.ValueSeparator) {
throw new SyntaxError(buildErrorMessage(token2));
}
}
}
#parseString(value: {
type: tokenType.string;
type: TokenType.String;
sourceText: string;
position: number;
}): string {
Expand All @@ -339,10 +333,10 @@ class JSONCParser {
return parsed;
}
#parseNullOrTrueOrFalseOrNumber(value: {
type: tokenType.nullOrTrueOrFalseOrNumber;
type: TokenType.NullOrTrueOrFalseOrNumber;
sourceText: string;
position: number;
}) {
}): null | boolean | number {
if (value.sourceText === "null") {
return null;
}
Expand All @@ -364,29 +358,29 @@ class JSONCParser {
}
}

function buildErrorMessage({ type, sourceText, position }: Token) {
function buildErrorMessage({ type, sourceText, position }: Token): string {
let token = "";
switch (type) {
case tokenType.beginObject:
case TokenType.BeginObject:
token = "{";
break;
case tokenType.endObject:
case TokenType.EndObject:
token = "}";
break;
case tokenType.beginArray:
case TokenType.BeginArray:
token = "[";
break;
case tokenType.endArray:
case TokenType.EndArray:
token = "]";
break;
case tokenType.nameSeparator:
case TokenType.NameSeparator:
token = ":";
break;
case tokenType.valueSeparator:
case TokenType.ValueSeparator:
token = ",";
break;
case tokenType.nullOrTrueOrFalseOrNumber:
case tokenType.string:
case TokenType.NullOrTrueOrFalseOrNumber:
case TokenType.String:
// Truncate the string so that it is within 30 lengths.
token = 30 < sourceText.length
? `${sourceText.slice(0, 30)}...`
Expand Down