Repository: syuilo/aiscript
Pull request #279 (2023-06-05 14:41:09)

パーサーのテンプレートの部分を改修
parser.peggyのTmplの定義を、
- {}の中身が無効な文字列だった場合、パースエラーになるようにしました。
- 関数を使わないような形に簡略化しました。

ついでにテストを少し修正しました。

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/syuilo/aiscript/pull/279?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) Report
Patch coverage has no change and project coverage change: **`+0.10`** :tada:
> Comparison is base [(`8ab9c27`)](https://app.codecov.io/gh/syuilo/aiscript/commit/8ab9c279009fe8453c9a65cf3bb7b359d081bb56?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) 88.34% compared to head [(`8fe90ff`)](https://app.codecov.io/gh/syuilo/aiscript/pull/279?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) 88.44%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #279      +/-   ##
==========================================
+ Coverage   88.34%   88.44%   +0.10%     
==========================================
  Files          19       19              
  Lines        2916     2916              
  Branches      485      492       +7     
==========================================
+ Hits         2576     2579       +3     
+ Misses        331      328       -3     
  Partials        9        9              
```


[see 1 file with indirect coverage changes](https://app.codecov.io/gh/syuilo/aiscript/pull/279/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/syuilo/aiscript/pull/279?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3150d1..3f5661d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 [Read translated version (en)](./translations/en/CHANGELOG.md)
 
 - オブジェクトの値を配列化する`Obj:vals`を追加
+- 文字列が`Json:parse`でパース可能であるかを判定する関数`Json:parsable`を追加
 
 # 0.13.3
 - 乱数を生成するとき引数の最大値を戻り値に含むように
diff --git a/docs/std.md b/docs/std.md
index 7fa5d3f..ffa17fb 100644
--- a/docs/std.md
+++ b/docs/std.md
@@ -33,6 +33,9 @@ JSONを生成します。
 ### @Json:parse(_json_: str): value
 JSONをパースします。  
 
+### @Json:parsable(_str_: str): bool
+文字列がJSONとしてパース可能であるかの判定を行います。 
+
 ## :: Date
 
 ### @Date:now(): num
diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts
index bec9de8..cfb70ac 100644
--- a/src/interpreter/lib/std.ts
+++ b/src/interpreter/lib/std.ts
@@ -148,6 +148,16 @@ export const std: Record<string, Value> = {
 		assertString(json);
 		return jsToVal(JSON.parse(json.value));
 	}),
+
+	'Json:parsable': FN_NATIVE(([str]) => {
+		assertString(str);
+		try {
+			JSON.parse(str.value);
+		} catch (e) {
+			return BOOL(false);
+		}
+		return BOOL(true);
+	}),
 	//#endregion
 
 	//#region Date
diff --git a/src/parser/parser.peggy b/src/parser/parser.peggy
index aeb7127..a90b2f5 100644
--- a/src/parser/parser.peggy
+++ b/src/parser/parser.peggy
@@ -1,36 +1,4 @@
 {
-	function group(arr, predicate) {
-		const dest = [];
-
-		for (let i = 0; i < arr.length; i++) {
-			if (i != 0 && predicate(arr[i - 1], arr[i])) {
-				dest[dest.length - 1].push(arr[i]);
-			} else {
-				dest.push([arr[i]]);
-			}
-		}
-
-		return dest;
-	}
-
-	function ungroup(groupes) {
-		return groupes.reduce((acc, val) => acc.concat(val), []);
-	}
-
-	function concatTemplate(arr) {
-		let groupes = group(arr, (prev, current) => (typeof current == typeof prev));
-
-		// concat string
-		groupes = groupes.map(g => {
-			if (typeof g[0] == 'string') {
-				return [g.join('')];
-			}
-			return g;
-		});
-
-		return ungroup(groupes);
-	}
-
 	function createNode(type, params, children) {
 		const node = { type };
 		params.children = children;
@@ -415,19 +383,19 @@ Identifier
 // template literal
 
 Tmpl
-	= "`" items:(!"`" i:TmplEmbed { return i; })* "`"
-{ return createNode('tmpl', { tmpl: concatTemplate(items) }); }
+	= "`" items:(!"`" @TmplEmbed)* "`"
+{ return createNode('tmpl', { tmpl: items }); }
 
 TmplEmbed
-	= TmplEsc
-	/ "{" __* expr:Expr __* "}"
-{ return expr; }
-	/ .
+	= "{" __* @expr:Expr __* "}"
+	/ str:TmplAtom+ {return str.join("")}
+
+TmplAtom
+  = TmplEsc
+	/ [^`{]
 
 TmplEsc
-	= "\\{" { return text()[1]; } // avoid syntax error of PEG.js (bug?)
-	/ "\\}" { return text()[1]; } // avoid syntax error of PEG.js (bug?)
-	/ "\\`" { return '`'; }
+	= "\\" @[{}`]
 
 // string literal
 


Test patch:
diff --git a/test/index.ts b/test/index.ts
index 0e4f2da..db7b601 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -957,6 +957,18 @@ describe('Template syntax', () => {
 		eq(res, STR('1 + 1 = 2'));
 	});
 
+	test.concurrent('invalid', async () => {
+		try {
+			await exe(`
+			<: \`{hoge}\`
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
 	test.concurrent('Escape', async () => {
 		const res = await exe(`
 		let message = "Hello"
@@ -966,7 +978,7 @@ describe('Template syntax', () => {
 	});
 });
 
-test.concurrent('Throws error when divied by zero', async () => {
+test.concurrent('Throws error when divided by zero', async () => {
 	try {
 		await exe(`
 		<: (0 / 0)
@@ -2491,8 +2503,8 @@ describe('std', () => {
 				let random = Math:gen_rng(seed)
 				return random(0 100)
 			}
-			let seed1 = \`{@Util:uuid()}\`
-			let seed2 = \`{@Date:year()}\`
+			let seed1 = \`{Util:uuid()}\`
+			let seed2 = \`{Date:year()}\`
 			let test1 = if (test(seed1) == test(seed1)) {true} else {false}
 			let test2 = if (test(seed1) == test(seed2)) {true} else {false}
 			<: [test1 test2]
@@ -2550,6 +2562,37 @@ describe('std', () => {
 			`);
 			eq(res, STR('"<function>"'));
 		});
+
+		test.concurrent('parsable', async () => {
+			[
+				'',
+				'null',
+				'hoge',
+				'"hoge"',
+				'[',
+				'[]',
+				'{}'
+			].forEach(async (str)=>{
+				const res=await exe(`
+					<: Json:parsable('${str}')
+				`);
+				assert.deepEqual(res.type, 'bool');
+				if (res.value){
+					await exe(`
+						<: Json:parse('${str}')
+					`);
+				} else {
+					try {
+						await exe(`
+							<: Json:parse('${str}')
+						`);
+					} catch (e) {
+						if (e instanceof SyntaxError) return;
+					}
+					assert.fail()
+				}
+			}):
+		});
 	});
 });