[PATCH v14 0/3] MR10897: vbscript: Handle declaration scope in the parser.
ClassDeclaration was only accepted at the top SourceElement level, so a prior statement on the same line (e.g. Dim x : Class C) failed to parse instead of being accepted or, for a name collision, reported as err 1041. Make ClassDeclaration a SimpleStatement and register it as a class at compile time in source order. Sub, Function and Class declarations are only valid at script global scope. A global If/ElseIf/Else/Select block still hoists a Sub or Function to global scope, but a loop (For/For Each/While/Do) or With block does not, and a Class is never hoisted. Reject the disallowed cases during parsing, reporting the location native reports rather than failing later during bytecode emission. -- v14: vbscript/tests: Add tests for sub declaration scope. vbscript: Handle class declaration scope in the parser. https://gitlab.winehq.org/wine/wine/-/merge_requests/10897
From: Francis De Brabandere <francisdb@gmail.com> A Class declaration is only valid at script global scope. Test that one is accepted after another statement on the same line (e.g. Dim x : Class C), that a duplicate name is reported at the later declaration, and that a Class declared in a procedure or control-flow body is rejected. --- dlls/vbscript/tests/run.c | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/dlls/vbscript/tests/run.c b/dlls/vbscript/tests/run.c index d9d7084a887..2d43651af84 100644 --- a/dlls/vbscript/tests/run.c +++ b/dlls/vbscript/tests/run.c @@ -3674,6 +3674,48 @@ static void test_external_caller_method_error(void) CHECK_CALLED(OnScriptError); } +static void test_class_decl_scope(void) +{ + static const struct { + const WCHAR *src; + BOOL expect_ok; /* whether the script should compile */ + USHORT error_code; /* expected error number when it should not */ + ULONG error_line; /* expected 0-based error line when it should not */ + BOOL todo; + } tests[] = { + /* A Class declaration may follow another statement separated by ':'. */ + { L"Dim x : Class C\nPublic v\nEnd Class\n", TRUE, 0, 0, TRUE }, + /* A Class declared inside a procedure body is rejected. */ + { L"Sub S\nClass C\nEnd Class\nEnd Sub\n", FALSE, 1002, 1, TRUE }, + /* A Class declared inside a control-flow block is rejected. */ + { L"If True Then\nClass C\nEnd Class\nEnd If\n", FALSE, 1002, 1, TRUE }, + /* A duplicate Class name is reported at the later declaration. */ + { L"Class C\nEnd Class\nDim x : Class C\nEnd Class\n", FALSE, 1041, 2, TRUE }, + }; + HRESULT hres; + unsigned i; + BOOL pass; + + for (i = 0; i < ARRAY_SIZE(tests); i++) { + error_line = ~0; + error_code = 0; + onerror_hres = S_OK; + SET_EXPECT(OnScriptError); + hres = parse_script_wr(tests[i].src); + CLEAR_CALLED(OnScriptError); + + if (tests[i].expect_ok) + pass = hres == S_OK; + else + pass = FAILED(hres) && error_code == tests[i].error_code + && error_line == tests[i].error_line; + + todo_wine_if(tests[i].todo) + ok(pass, "[%u] %s: hres=%08lx code=%u line=%lu\n", i, wine_dbgstr_w(tests[i].src), + hres, error_code, error_line); + } +} + static void test_msgbox(void) { HRESULT hres; @@ -4370,6 +4412,7 @@ static void run_tests(void) test_isexpression(); test_option_explicit_errors(); test_parse_errors(); + test_class_decl_scope(); test_redefine_scope(); test_getref_error_reporting(); test_getref_external_caller_error(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10897
From: Francis De Brabandere <francisdb@gmail.com> A ClassDeclaration was only accepted as a standalone top-level SourceElement, so a class that followed another statement on the same line (e.g. Dim x : Class C) failed to parse, and a duplicate class name was reported at the first declaration rather than the later one. Make a class one of the units a global line is built from, registered in source order, so such a line parses and a redefinition is reported at the later declaration as native does. Because a class is only valid at global scope it is reachable only there, not from the shared statement grammar used by procedure and control-flow bodies, so a class anywhere else is reported as a syntax error at its location. --- dlls/vbscript/parser.y | 25 ++++++++++++++++++++----- dlls/vbscript/tests/run.c | 21 ++++++++------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/dlls/vbscript/parser.y b/dlls/vbscript/parser.y index ec67cde2215..fea607a8d63 100644 --- a/dlls/vbscript/parser.y +++ b/dlls/vbscript/parser.y @@ -185,8 +185,13 @@ SourceElements : /* empty */ | SourceElements GlobalDimDeclaration StSep { source_add_statement(ctx, $2); } - | SourceElements StatementNl { source_add_statement(ctx, $2); } - | SourceElements ClassDeclaration { source_add_class(ctx, $2); } + | SourceElements SimpleStatement StSep { source_add_statement(ctx, $2); } + | SourceElements ClassDeclaration StSep { source_add_class(ctx, $2); } + +/* A Class declaration is reachable only here at script global scope, not from + the shared SimpleStatement used by every body, so a Class anywhere else is + rejected as a syntax error (see the tCLASS rule in SimpleStatement), while + still being allowed after another statement, e.g. Dim x : Class C. */ GlobalDimDeclaration : tPRIVATE tCONST ConstDeclList { $$ = new_const_statement(ctx, @$, $3); CHECK_ERROR; } @@ -267,6 +272,12 @@ SimpleStatement | tDO StSep StatementsNl_opt error { ctx->hres = MAKE_VBSERROR(VBSE_EXPECTED_LOOP); YYABORT; } | tDO error { ctx->hres = MAKE_VBSERROR(VBSE_EXPECTED_WHILE_UNTIL_EOS); YYABORT; } | FunctionDecl { $$ = new_function_statement(ctx, @$, $1); CHECK_ERROR; } + | tCLASS { /* A Class is only valid at global scope (see SourceElements). The real + ClassDeclaration wins the shift there, so this rule only matches in a + body, where it reports the syntax error native reports. It is needed: + without it, error recovery surfaces a different, context-dependent + error code instead of the plain syntax error. */ + ctx->error_loc = @1; ctx->hres = MAKE_VBSERROR(VBSE_SYNTAX_ERROR); YYABORT; } | tEXIT tDO { $$ = new_statement(ctx, STAT_EXITDO, 0, @2); CHECK_ERROR; } | tEXIT tFOR { $$ = new_statement(ctx, STAT_EXITFOR, 0, @2); CHECK_ERROR; } | tEXIT tFUNCTION { $$ = new_statement(ctx, STAT_EXITFUNC, 0, @2); CHECK_ERROR; } @@ -574,7 +585,7 @@ PrimaryExpression | tME { $$ = new_expression(ctx, EXPR_ME, 0); CHECK_ERROR; } ClassDeclaration - : tCLASS Identifier StSep ClassBody tEND tCLASS StSep { $4->name = $2; $4->loc = @2; $$ = $4; } + : tCLASS Identifier StSep ClassBody tEND tCLASS { $4->name = $2; $4->loc = @2; $$ = $4; } | tCLASS Identifier tEND tCLASS { ctx->error_loc = @3; ctx->hres = MAKE_VBSERROR(VBSE_EXPECTED_STATEMENT); YYABORT; } | tCLASS Identifier StSep ClassBody tEND error { ctx->hres = MAKE_VBSERROR(VBSE_EXPECTED_CLASS); YYABORT; } @@ -733,8 +744,12 @@ static void source_add_statement(parser_ctx_t *ctx, statement_t *stat) static void source_add_class(parser_ctx_t *ctx, class_decl_t *class_decl) { - class_decl->next = ctx->class_decls; - ctx->class_decls = class_decl; + class_decl_t **iter; + + /* Append to keep classes in source order, so a redefinition is reported at the later declaration. */ + class_decl->next = NULL; + for(iter = &ctx->class_decls; *iter; iter = &(*iter)->next); + *iter = class_decl; } static void handle_isexpression_script(parser_ctx_t *ctx, expression_t *expr) diff --git a/dlls/vbscript/tests/run.c b/dlls/vbscript/tests/run.c index 2d43651af84..30a099d1a5f 100644 --- a/dlls/vbscript/tests/run.c +++ b/dlls/vbscript/tests/run.c @@ -3681,20 +3681,18 @@ static void test_class_decl_scope(void) BOOL expect_ok; /* whether the script should compile */ USHORT error_code; /* expected error number when it should not */ ULONG error_line; /* expected 0-based error line when it should not */ - BOOL todo; } tests[] = { /* A Class declaration may follow another statement separated by ':'. */ - { L"Dim x : Class C\nPublic v\nEnd Class\n", TRUE, 0, 0, TRUE }, + { L"Dim x : Class C\nPublic v\nEnd Class\n", TRUE }, /* A Class declared inside a procedure body is rejected. */ - { L"Sub S\nClass C\nEnd Class\nEnd Sub\n", FALSE, 1002, 1, TRUE }, + { L"Sub S\nClass C\nEnd Class\nEnd Sub\n", FALSE, 1002, 1 }, /* A Class declared inside a control-flow block is rejected. */ - { L"If True Then\nClass C\nEnd Class\nEnd If\n", FALSE, 1002, 1, TRUE }, + { L"If True Then\nClass C\nEnd Class\nEnd If\n", FALSE, 1002, 1 }, /* A duplicate Class name is reported at the later declaration. */ - { L"Class C\nEnd Class\nDim x : Class C\nEnd Class\n", FALSE, 1041, 2, TRUE }, + { L"Class C\nEnd Class\nDim x : Class C\nEnd Class\n", FALSE, 1041, 2 }, }; HRESULT hres; unsigned i; - BOOL pass; for (i = 0; i < ARRAY_SIZE(tests); i++) { error_line = ~0; @@ -3705,14 +3703,11 @@ static void test_class_decl_scope(void) CLEAR_CALLED(OnScriptError); if (tests[i].expect_ok) - pass = hres == S_OK; + ok(hres == S_OK, "[%u] %s: hres=%08lx\n", i, wine_dbgstr_w(tests[i].src), hres); else - pass = FAILED(hres) && error_code == tests[i].error_code - && error_line == tests[i].error_line; - - todo_wine_if(tests[i].todo) - ok(pass, "[%u] %s: hres=%08lx code=%u line=%lu\n", i, wine_dbgstr_w(tests[i].src), - hres, error_code, error_line); + ok(FAILED(hres) && error_code == tests[i].error_code && error_line == tests[i].error_line, + "[%u] %s: hres=%08lx code=%u line=%lu\n", i, wine_dbgstr_w(tests[i].src), + hres, error_code, error_line); } } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10897
From: Francis De Brabandere <francisdb@gmail.com> A Sub or Function is only valid at script global scope: a global If/Select block hoists it to global scope, but a loop or procedure body does not. Test the hoisting cases, and mark todo_wine the disallowed cases that native rejects but the parser does not yet (a Sub in another procedure body or in a loop), since handling those is left to a follow-up. --- dlls/vbscript/tests/run.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/dlls/vbscript/tests/run.c b/dlls/vbscript/tests/run.c index 30a099d1a5f..5bdacbb4873 100644 --- a/dlls/vbscript/tests/run.c +++ b/dlls/vbscript/tests/run.c @@ -3711,6 +3711,44 @@ static void test_class_decl_scope(void) } } +/* Like a Class, a Sub or Function is only valid at script global scope (a + global If/Select block hoists it, a loop or procedure body does not). Native + rejects the disallowed cases; the parser does not yet, so these are todo. */ +static void test_sub_decl_scope(void) +{ + static const struct { + const WCHAR *src; + BOOL expect_ok; /* whether the script should compile */ + USHORT error_code; /* expected native error number when it should not */ + ULONG error_line; /* expected 0-based native error line when it should not */ + BOOL todo; /* the parser does not yet reject this case */ + } tests[] = { + { L"If False Then\nSub S\nEnd Sub\nEnd If\nCall S\n", TRUE, 0, 0, FALSE }, + { L"Select Case 1\nCase 1\nSub S\nEnd Sub\nEnd Select\nCall S\n", TRUE, 0, 0, FALSE }, + { L"Sub S\nSub T\nEnd Sub\nEnd Sub\n", FALSE, 1002, 1, TRUE }, + { L"For i = 1 To 1\nSub S\nEnd Sub\nNext\n", FALSE, 1002, 1, TRUE }, + }; + HRESULT hres; + unsigned i; + + for (i = 0; i < ARRAY_SIZE(tests); i++) { + error_line = ~0; + error_code = 0; + onerror_hres = S_OK; + SET_EXPECT(OnScriptError); + hres = parse_script_wr(tests[i].src); + CLEAR_CALLED(OnScriptError); + + if (tests[i].expect_ok) + ok(hres == S_OK, "[%u] %s: hres=%08lx\n", i, wine_dbgstr_w(tests[i].src), hres); + else + todo_wine_if(tests[i].todo) + ok(FAILED(hres) && error_code == tests[i].error_code && error_line == tests[i].error_line, + "[%u] %s: hres=%08lx code=%u line=%lu\n", i, wine_dbgstr_w(tests[i].src), + hres, error_code, error_line); + } +} + static void test_msgbox(void) { HRESULT hres; @@ -4408,6 +4446,7 @@ static void run_tests(void) test_option_explicit_errors(); test_parse_errors(); test_class_decl_scope(); + test_sub_decl_scope(); test_redefine_scope(); test_getref_error_reporting(); test_getref_external_caller_error(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10897
On Mon Jun 8 09:42:32 2026 +0000, Francis De Brabandere wrote:
changed this line in [version 14 of the diff](/wine/wine/-/merge_requests/10897/diffs?diff_id=273513&start_sha=ec856d1dbb9a4b9ed05ebb08fe6dad84d03f33e1#901fca57b1489d9d8feed68e5d67555dffc7cfe7_188_188) Adjusted as requested.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10897#note_142392
participants (2)
-
Francis De Brabandere -
Francis De Brabandere (@francisdb)