diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java index 5e959609..b6c4860c 100644 --- a/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java +++ b/src/main/java/com/alibaba/qlexpress4/aparser/OutVarNamesVisitor.java @@ -113,16 +113,6 @@ public Void visitVariableDeclaratorId(QLParser.VariableDeclaratorIdContext ctx) return null; } - @Override - public Void visitForEachStatement(QLParser.ForEachStatementContext ctx) { - ctx.expression().accept(this); - push(); - getStack().add(ctx.varId().getText()); - ctx.blockStatements().accept(this); - pop(); - return null; - } - @Override public Void visitExpression(QLParser.ExpressionContext ctx) { QLParser.TernaryExprContext ternaryExprContext = ctx.ternaryExpr(); diff --git a/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java b/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java index 5e575c8b..543b569a 100644 --- a/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java +++ b/src/main/java/com/alibaba/qlexpress4/aparser/ScopeStackVisitor.java @@ -110,14 +110,66 @@ public Void visitTryCatchExpr(QLParser.TryCatchExprContext ctx) { return null; } + @Override + public Void visitForEachStatement(QLParser.ForEachStatementContext ctx) { + ctx.expression().accept(this); + push(); + getStack().add(ctx.varId().getText()); + ctx.blockStatements().accept(this); + pop(); + return null; + } + + @Override + public Void visitLambdaExpr(QLParser.LambdaExprContext ctx) { + push(); + QLParser.LambdaParametersContext params = ctx.lambdaParameters(); + if (params.varId() != null) { + getStack().add(params.varId().getText()); + } + else if (params.formalOrInferredParameterList() != null) { + params.formalOrInferredParameterList().accept(this); + } + if (ctx.blockStatements() != null) { + ctx.blockStatements().accept(this); + } + else if (ctx.expression() != null) { + ctx.expression().accept(this); + } + pop(); + return null; + } + @Override public Void visitTryCatch(QLParser.TryCatchContext ctx) { push(); - super.visitTryCatch(ctx); + if (ctx.catchParams() != null && ctx.catchParams().varId() != null) { + getStack().add(ctx.catchParams().varId().getText()); + } + ctx.blockStatements().accept(this); pop(); return null; } - + + @Override + public Void visitTraditionalForStatement(QLParser.TraditionalForStatementContext ctx) { + push(); + if (ctx.forInit() != null) { + ctx.forInit().accept(this); + } + if (ctx.forCondition != null) { + ctx.forCondition.accept(this); + } + if (ctx.forUpdate != null) { + ctx.forUpdate.accept(this); + } + if (ctx.blockStatements() != null) { + ctx.blockStatements().accept(this); + } + pop(); + return null; + } + @Override public Void visitFunctionStatement(QLParser.FunctionStatementContext ctx) { ctx.varId().accept(this); diff --git a/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java b/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java index 49772ec0..6438f319 100644 --- a/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java +++ b/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java @@ -1247,6 +1247,79 @@ public void getOutVarNamesForEachTargetUsesOuterScopeTest() { express4Runner.getOutVarNames("for(i : i) {\n" + " if(i > 0) { return i; }\n" + "}\n" + "return 0;"); Assert.assertEquals(Collections.singleton("i"), actual); } + + @Test + public void getOutVarNamesLambdaFormalParamsTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Lambda parameters (a, b) should not leak into outer scope + // Only 'x' and 'y' should be reported as external variables + Set actual = express4Runner.getOutVarNames( + "f = (a, b) -> a + b;\n" + "return f(x, y);"); + Set expected = new HashSet<>(); + expected.add("x"); + expected.add("y"); + Assert.assertEquals(expected, actual); + } + + @Test + public void getOutVarNamesLambdaSingleParamTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Single-parameter lambda: 'x' is a lambda param, not external + // Only 'y' should be reported as external + Set actual = express4Runner.getOutVarNames( + "f = x -> x + 1;\n" + "return f(y);"); + Assert.assertEquals(Collections.singleton("y"), actual); + } + + @Test + public void getOutVarNamesLambdaParamNotLeakTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Lambda param 'a' should not leak: after the lambda, + // 'a' used in 'a + 1' should be flagged as external + Set actual = express4Runner.getOutVarNames( + "f = (a) -> a + 1;\n" + "return a + 1;"); + Assert.assertEquals(Collections.singleton("a"), actual); + } + + @Test + public void getOutVarNamesCatchVariableTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Catch variable 'e' should not be reported as external + // Only 'x' (used in try block) should be external + Set actual = express4Runner.getOutVarNames( + "try {\n" + " println(x);\n" + "} catch(e) {\n" + " println(e);\n" + "}"); + Assert.assertEquals(Collections.singleton("x"), actual); + } + + @Test + public void getOutVarNamesCatchVariableMultipleCatchesTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Multiple catch blocks: each catch variable should be scoped + Set actual = express4Runner.getOutVarNames( + "try {\n" + " println(x);\n" + + "} catch(RuntimeException e1) {\n" + " println(e1);\n" + + "} catch(Exception e2) {\n" + " println(e2);\n" + "}"); + Assert.assertEquals(Collections.singleton("x"), actual); + } + + @Test + public void getOutVarNamesTraditionalForLoopTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // Traditional for-loop variable 'i' should not leak into outer scope + // 'n' is external, 'i' is local to the loop + Set actual = express4Runner.getOutVarNames( + "int sum = 0;\n" + "for(int i = 0; i < n; i++) {\n" + " sum += i;\n" + "}\n" + "return sum;"); + Assert.assertEquals(Collections.singleton("n"), actual); + } + + @Test + public void getOutVarNamesTraditionalForLoopVarNotLeakTest() { + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // After the for-loop, 'i' should be flagged as external if used + Set actual = express4Runner.getOutVarNames( + "for(int i = 0; i < 10; i++) {\n" + " println(i);\n" + "}\n" + "return i;"); + Assert.assertEquals(Collections.singleton("i"), actual); + } @Test public void getOutVarAttrsTest() {