Repository: aiscript-dev/aiscript (ts)

Issue:
属性名に予約語が使用できてしまう
属性名に予約語を使用した以下のようなコードが通ってしまうが、
```js
#[class]
let a = 42
```
このコードを見ると意図としては通らないのが正しそう。
https://github.com/aiscript-dev/aiscript/blob/e7e2651ac6928bc2e766cd21a7e4ed5029cd481b/src/parser/plugins/validate-keyword.ts#L88-L96
原因は`visitNode`が`Definition`の`attr`メンバを見ていないことだと考えられる。
https://github.com/aiscript-dev/aiscript/blob/e7e2651ac6928bc2e766cd21a7e4ed5029cd481b/src/parser/visit.ts#L8-L11


Pull request description:
属性名の予約語を禁止
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、以下をご確認ください:
- 可能であればタイトルに、以下で示すようなPRの種類が分かるキーワードをプリフィクスしてください。
  - fix / refactor / feat / enhance / perf / chore
  - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
- このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
- unreleasedディレクトリ以下にファイルを新規に作成し、変更点を記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
- この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
- 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
- テスト、Lintが通っていることを予め確認してください。
  - `npm run test`、`npm run lint`でぞれぞれ実施可能です
- `npm run api`を実行してAPIレポートを更新し、差分がある場合はコミットしてください。
ご協力ありがとうございます🤗
-->
<!-- ℹ README
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
  - fix / refactor / feat / enhance / perf / chore
  - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please create a new file under the `unreleased` directory and write down the summary of your changes. However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
  - You can run it with `npm run test` and `npm run lint`.
- Run `npm run api` to update the API report and commit it if there are any diffs.
Thanks for your cooperation 🤗
-->

# What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->
属性名に予約語を使用すると文法エラーにします。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
- Resolves #812

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->
test/keywords.tsにおいて現在の文法において使用されていない予約語を追加します。
また、予約語をランダムに選択するのではなく、すべての語についてテストするようにします。
exeからparseに変えているのは気休め程度の負荷軽減……


Patch:
diff --git a/src/parser/visit.ts b/src/parser/visit.ts
index 36fcf3a..efe878b 100644
--- a/src/parser/visit.ts
+++ b/src/parser/visit.ts
@@ -11,6 +11,7 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node
 	// nested nodes
 	switch (result.type) {
 		case 'def': {
+			result.attr = result.attr.map((attr) => visitNodeInner(attr, fn, ancestors) as Ast.Attribute);
 			result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Definition['expr'];
 			break;
 		}


Test patch:
diff --git a/test/keywords.ts b/test/keywords.ts
index 1311fd4..ca9fca1 100644
--- a/test/keywords.ts
+++ b/test/keywords.ts
@@ -1,9 +1,9 @@
 import { describe, expect, test } from 'vitest';
-import { Parser, Interpreter } from '../src';
+import { Parser } from '../src';
 import { AiScriptSyntaxError } from '../src/error';
-import { exe } from './testutils';
 
 const reservedWords = [
+	// 使用中の語
 	'null',
 	'true',
 	'false',
@@ -25,9 +25,55 @@ const reservedWords = [
 	'var',
 	'let',
 	'exists',
+
+	// 使用予定の語
+	// 文脈キーワードは識別子に利用できるため除外
+	'as',
+	'async',
+	'attr',
+	'attribute',
+	'await',
+	'catch',
+	'class',
+	// 'const',
+	'component',
+	'constructor',
+	// 'def',
+	'dictionary',
+	'enum',
+	'export',
+	'finally',
+	'fn',
+	// 'func',
+	// 'function',
+	'hash',
+	'in',
+	'interface',
+	'out',
+	'private',
+	'public',
+	'ref',
+	'static',
+	'struct',
+	'table',
+	'this',
+	'throw',
+	'trait',
+	'try',
+	'undefined',
+	'use',
+	'using',
+	'when',
+	'yield',
+	'import',
+	'is',
+	'meta',
+	'module',
+	'namespace',
+	'new',
 ] as const;
 
-const sampleCodes = Object.entries({
+const sampleCodes = Object.entries<(word: string) => string>({
 	variable: word =>
 	`
 	let ${word} = "ai"
@@ -66,24 +112,22 @@ const sampleCodes = Object.entries({
 	`,
 });
 
-function pickRandom<T>(arr: T[]): T {
-	return arr[Math.floor(Math.random() * arr.length)];
-}
+const parser = new Parser();
 
 describe.each(
 	sampleCodes
 )('reserved word validation on %s', (_, sampleCode) => {
 
 	test.concurrent.each(
-		[pickRandom(reservedWords)]
+		reservedWords
 	)('%s must be rejected', (word) => {
-		return expect(exe(sampleCode(word))).rejects.toThrow(AiScriptSyntaxError);
+		expect(() => parser.parse(sampleCode(word))).toThrow(AiScriptSyntaxError);
 	});
 
 	test.concurrent.each(
-		[pickRandom(reservedWords)]
+		reservedWords
 	)('%scat must be allowed', (word) => {
-		return exe(sampleCode(word+'cat'));
+		parser.parse(sampleCode(word+'cat'));
 	});
 
 });