Skip to content
Merged
Show file tree
Hide file tree
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
30 changes: 30 additions & 0 deletions src/parser/visit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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': {
Expand All @@ -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();
Expand Down
29 changes: 26 additions & 3 deletions test/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
}
});

Expand Down
31 changes: 18 additions & 13 deletions test/jump-statements.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down
16 changes: 12 additions & 4 deletions test/testutils.ts
Original file line number Diff line number Diff line change
@@ -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<Value | undefined> {
const parser = new Parser();
Expand Down Expand Up @@ -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);
}
};

14 changes: 12 additions & 2 deletions test/types.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});
2 changes: 2 additions & 0 deletions unreleased/fix-plugins-not-visiting-some-nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fix: break文のもつ値の式について不正な変数やreturn文、break文、continue文、型注釈がある場合に文法エラーにならない問題を修正
- Fix: 型注釈における型引数や関数型の返り値に不正な型注釈がある場合に文法エラーにならない問題を修正
Loading