diff --git a/src/parser/visit.ts b/src/parser/visit.ts index a5686997..0807f302 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -152,6 +152,12 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Prop['target']; break; } + case 'break': { + if (result.expr != null) { + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Break['expr']; + } + break; + } case 'ns': { for (let i = 0; i < result.members.length; i++) { result.members[i] = visitNodeInner(result.members[i]!, fn, ancestors) as (typeof result.members)[number]; @@ -208,10 +214,17 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node break; } + case 'namedTypeSource': { + if (result.inner != null) { + result.inner = visitNodeInner(result.inner, fn, ancestors) as Ast.NamedTypeSource['inner']; + } + break; + } case 'fnTypeSource': { for (let i = 0; i < result.params.length; i++) { result.params[i] = visitNodeInner(result.params[i]!, fn, ancestors) as Ast.FnTypeSource['params'][number]; } + result.result = visitNodeInner(result.result, fn, ancestors) as Ast.FnTypeSource['result']; break; } case 'unionTypeSource': { @@ -220,6 +233,23 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node } break; } + + case 'str': + case 'num': + case 'bool': + case 'null': + case 'identifier': + case 'attr': + case 'continue': + case 'meta': { + break; // nop + } + + default: { + // exhaustiveness check + result satisfies never; + throw new Error('invalid node type'); + } } ancestors.pop(); diff --git a/test/identifiers.ts b/test/identifiers.ts index 1097558d..842beb9e 100644 --- a/test/identifiers.ts +++ b/test/identifiers.ts @@ -253,10 +253,30 @@ const sampleCodes = Object.entries<[(definedName: string, referredName: string) } `, NULL], + break: [(definedName, referredName) => + ` + <: #label: eval { + break #label eval { + let ${definedName} = "ai" + ${referredName} + } + } + `, STR('ai')], + typeParam: [(definedName, referredName) => ` @f<${definedName}>(x): ${referredName} { x } `, NULL], + + innerType: [(definedName, referredName) => + ` + let x: arr<@<${definedName}>() => ${referredName}> = [] + `, NULL], + + returnType: [(definedName, referredName) => + ` + let x: @() => @<${definedName}>() => ${referredName} = @() {} + `, NULL], }); const parser = new Parser(); @@ -278,15 +298,18 @@ describe.each( parser.parse(sampleCode(wordCat, wordCat)); }); - test.concurrent.each( + // グローバルの expect を使用すると expect.hasAssertions() が失敗するときがあるので、 + // ローカルの expect を使用する + test.concurrent.for( identifierCases - )('%s is allowed: %s', async (word, allowed) => { + )('%s is allowed: %s', async ([word, allowed], { expect }) => { expect.hasAssertions(); if (allowed) { const res = await exe(sampleCode(word, word)); - eq(res, expected); + eq(res, expected, expect); } else { expect(() => parser.parse(sampleCode(word, word))).toThrow(AiScriptSyntaxError); + await Promise.resolve(); // https://github.com/vitest-dev/vitest/issues/4750 } }); diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 633474b6..14fb1065 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { describe, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { errors, utils } from '../src'; import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; import { AiScriptRuntimeError, AiScriptSyntaxError } from '../src/error'; @@ -507,21 +507,26 @@ describe('return', () => { await assert.rejects(() => exe('return eval { return 1 } + 2')); }); - test.concurrent('in break', async () => { - const res = await exe(` - @f() { + describe('in break', () => { + test.concurrent('valid', async () => { + const res = await exe(` + @f() { + #l: eval { + break #l eval { return 1 } + } + } + <: f() + `); + eq(res, NUM(1)); + }); + + test.concurrent('invalid', async () => { + await expect(() => exe(` #l: eval { break #l eval { return 1 } } - } - <: f() - `); - eq(res, NUM(1)); - await assert.rejects(() => exe(` - #l: eval { - break #l eval { return 1 } - } - `)); + `)).rejects.toThrow(AiScriptSyntaxError); + }); }); describe('in and', async () => { diff --git a/test/testutils.ts b/test/testutils.ts index d03133fa..0b09ac11 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -1,5 +1,6 @@ -import * as assert from 'assert'; +import { expect as globalExpect } from 'vitest'; import { Parser, Interpreter } from '../src'; +import { Value } from '../src/interpreter/value'; export async function exe(script: string): Promise { const parser = new Parser(); @@ -42,8 +43,15 @@ export const getMeta = (script: string) => { return metadata; }; -export const eq = (a, b) => { - assert.deepEqual(a.type, b.type); - assert.deepEqual(a.value, b.value); +export const eq = (a: Value | undefined, b: Value | undefined, expect = globalExpect) => { + expect(a).not.toBeUndefined(); + expect(b).not.toBeUndefined(); + expect(a!.type).toEqual(b!.type); + if ('value' in a!) { + expect('value' in b!).toBe(true); + expect(a.value).toEqual((b as { value: unknown }).value); + } else { + expect('value' in b!).toBe(false); + } }; diff --git a/test/types.ts b/test/types.ts index d876e00a..e717e809 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,8 +1,8 @@ import * as assert from 'assert'; -import { describe, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { utils } from '../src'; import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; -import { AiScriptRuntimeError } from '../src/error'; +import { AiScriptRuntimeError, AiScriptSyntaxError } from '../src/error'; import { exe, getMeta, eq } from './testutils'; describe('function types', () => { @@ -198,3 +198,13 @@ describe('simple', () => { eq(res, NUM(1)); }); }); + +test.concurrent('in break', async () => { + await expect(() => exe(` + #l: eval { + break #l eval { + let x: invalid = 0 + } + } + `)).rejects.toThrow(AiScriptSyntaxError); +}); diff --git a/unreleased/fix-plugins-not-visiting-some-nodes.md b/unreleased/fix-plugins-not-visiting-some-nodes.md new file mode 100644 index 00000000..ce2c30a4 --- /dev/null +++ b/unreleased/fix-plugins-not-visiting-some-nodes.md @@ -0,0 +1,2 @@ +- Fix: break文のもつ値の式について不正な変数やreturn文、break文、continue文、型注釈がある場合に文法エラーにならない問題を修正 +- Fix: 型注釈における型引数や関数型の返り値に不正な型注釈がある場合に文法エラーにならない問題を修正