* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "assembly-function.h"
#include "assembly-program.h"
#include "ir/astNode.h"
#include "public/es2panda_lib.h"
#include "public/public.h"
#include "test/utils/asm_test.h"
#include "test/utils/ast_verifier_test.h"
#ifndef ES2PANDA_BIN_PATH
#error "ES2PANDA_BIN_PATH is not defined (pass it from CMakeLists.txt)"
#endif
#if defined(__has_feature)
#if __has_feature(address_sanitizer)
#define ES2PANDA_TEST_WITH_ASAN 1
#endif
#endif
#if defined(__SANITIZE_ADDRESS__)
#define ES2PANDA_TEST_WITH_ASAN 1
#endif
namespace ark::es2panda::compiler::test {
class ScopeLineInfoTest : public ::test::utils::AsmTest {
public:
struct LambdaInvokeGuardCheckResult {
bool sawLambdaInvoke {false};
bool sawNextUserLine {false};
};
const pandasm::Function *FindFunctionByName(const pandasm::Program &program, const std::string &name)
{
auto itS = program.functionStaticTable.find(name);
if (itS != program.functionStaticTable.end()) {
return &itS->second;
}
auto itI = program.functionInstanceTable.find(name);
if (itI != program.functionInstanceTable.end()) {
return &itI->second;
}
return nullptr;
}
static std::vector<std::string> SplitLines(std::string_view text)
{
std::vector<std::string> lines;
std::string cur;
cur.reserve(text.size());
for (char c : text) {
if (c == '\n') {
lines.emplace_back(cur);
cur.clear();
} else {
cur.push_back(c);
}
}
lines.emplace_back(cur);
return lines;
}
static uint32_t FindLineNo(const std::vector<std::string> &lines, const std::string &needle)
{
for (size_t i = 0; i < lines.size(); i++) {
if (lines[i].find(needle) != std::string::npos) {
return static_cast<uint32_t>(i + 1U);
}
}
return 0U;
}
template <class Fn>
static void ForEachFunction(const pandasm::Program &program, Fn &&cb)
{
for (const auto &[name, fn] : program.functionStaticTable) {
cb(name, fn);
}
for (const auto &[name, fn] : program.functionInstanceTable) {
cb(name, fn);
}
}
static bool IsIllegalLine(uint32_t ln) noexcept
{
return ln > static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
}
static bool ExpectNextUserLine(const pandasm::Function &func, size_t startIndex, uint32_t nextUserLine)
{
for (size_t j = startIndex; j < func.ins.size(); j++) {
const uint32_t lineNo = func.ins[j].insDebug.LineNumber();
if (IsIllegalLine(lineNo)) {
continue;
}
EXPECT_EQ(lineNo, nextUserLine) << "Step-out should continue on the next user-authored line.";
return true;
}
return false;
}
static LambdaInvokeGuardCheckResult CheckLambdaInvokeGuardLineInfo(const pandasm::Function &func, uint32_t callLine,
uint32_t nextUserLine)
{
LambdaInvokeGuardCheckResult result;
for (size_t i = 0; i + 2U < func.ins.size(); i++) {
const auto &ins = func.ins[i];
if (ins.opcode != pandasm::Opcode::CALL_VIRT_SHORT || ins.insDebug.LineNumber() != callLine) {
continue;
}
const auto &guardStore = func.ins[i + 1U];
const auto &guardCheckCast = func.ins[i + 2U];
if (guardStore.opcode != pandasm::Opcode::STA_OBJ || guardCheckCast.opcode != pandasm::Opcode::CHECKCAST) {
continue;
}
result.sawLambdaInvoke = true;
EXPECT_TRUE(IsIllegalLine(guardStore.insDebug.LineNumber())) <<
"The compiler-generated store after lambda invoke must not map to the source call line.";
EXPECT_TRUE(IsIllegalLine(guardCheckCast.insDebug.LineNumber())) <<
"The compiler-generated checkcast after lambda invoke must not map to the source call line.";
result.sawNextUserLine = ExpectNextUserLine(func, i + 3U, nextUserLine);
break;
}
return result;
}
};
class ScopeLineInfoCapiTest : public ::test::utils::AstVerifierTest {
public:
struct DebugLineSummary {
std::unordered_map<std::string, bool> functionAllLinesInvalid;
};
static bool IsIllegalLine(uint32_t ln) noexcept
{
return ln > static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
}
template <class Fn>
static void ForEachFunction(const pandasm::Program &program, Fn &&cb)
{
for (const auto &[name, fn] : program.functionStaticTable) {
cb(name, fn);
}
for (const auto &[name, fn] : program.functionInstanceTable) {
cb(name, fn);
}
}
static DebugLineSummary CollectDebugLineSummary(const pandasm::Program &program)
{
DebugLineSummary summary;
ForEachFunction(program, [&](const std::string &fnName, const pandasm::Function &func) {
summary.functionAllLinesInvalid.emplace(
fnName, !func.ins.empty() && std::all_of(func.ins.begin(), func.ins.end(), [](const auto &ins) {
return IsIllegalLine(ins.insDebug.LineNumber());
}));
});
return summary;
}
DebugLineSummary CompileWithFlag(std::string_view text, bool markFirstLambda)
{
CONTEXT(ES2PANDA_STATE_PARSED, text.data())
{
if (markFirstLambda) {
auto *programAst = reinterpret_cast<ir::AstNode *>(GetAst());
EXPECT_NE(programAst, nullptr);
if (programAst == nullptr) {
return DebugLineSummary {};
}
auto *lambda =
programAst->FindChild([](ir::AstNode *child) { return child->IsArrowFunctionExpression(); });
EXPECT_NE(lambda, nullptr);
if (lambda == nullptr) {
return DebugLineSummary {};
}
GetImpl()->AstNodeSetNoDebugLineFlag(GetContext(), reinterpret_cast<es2panda_AstNode *>(lambda));
}
GetImpl()->ProceedToState(GetContext(), ES2PANDA_STATE_ASM_GENERATED);
EXPECT_EQ(GetImpl()->ContextState(GetContext()), ES2PANDA_STATE_ASM_GENERATED) << ContextErrorMessage();
auto *internalCtx = reinterpret_cast<public_lib::Context *>(GetContext());
EXPECT_FALSE(internalCtx->output.empty());
EXPECT_NE(internalCtx->output.begin()->second, nullptr);
if (internalCtx->output.empty() || internalCtx->output.begin()->second == nullptr) {
return DebugLineSummary {};
}
return CollectDebugLineSummary(*internalCtx->output.begin()->second);
}
return DebugLineSummary {};
}
};
TEST_F(ScopeLineInfoTest, LoweringGeneratedCode_LineNumberValidityAndInvalidMarker)
{
std::string_view text = R"(// KNOWN ISSUE: the 1st line code is setted to invailid line number
let a = [1, 2, 3];
let b = [-1, ...a, -2];
(() => { console.log(a); })();
)";
std::array args = {
ES2PANDA_BIN_PATH,
"--debug-info=true",
"--opt-level=0",
"--ets-unnamed",
};
auto program = GetCurrentProgramWithArgs({args.data(), args.size()}, text);
ASSERT_NE(program, nullptr);
const auto lines = SplitLines(text);
const auto maxLine = static_cast<uint32_t>(lines.size());
bool sawIllegal = false;
ForEachFunction(*program, [&](const std::string &fnName, const pandasm::Function &func) {
for (const auto &ins : func.ins) {
const uint32_t ln = ins.insDebug.LineNumber();
if (IsIllegalLine(ln)) {
sawIllegal = true;
continue;
}
EXPECT_GE(ln, 1U) << "fn=" << fnName;
EXPECT_LE(ln, maxLine) << "fn=" << fnName;
}
});
EXPECT_TRUE(sawIllegal) << "At least one lowering-generated instructionto have an illegal line number.";
const auto *cctor = FindFunctionByName(*program, "ETSGLOBAL.<cctor>:void;");
ASSERT_NE(cctor, nullptr);
const uint32_t sprLine = FindLineNo(lines, "let b = [-1, ...a, -2]");
const uint32_t lamLine = FindLineNo(lines, "(() => {");
ASSERT_GT(sprLine, 0U);
ASSERT_GT(lamLine, 0U);
bool sawSpr = false;
bool sawLam = false;
for (const auto &ins : cctor->ins) {
const uint32_t ln = ins.insDebug.LineNumber();
if (ln == sprLine) {
sawSpr = true;
}
if (ln == lamLine) {
sawLam = true;
}
}
EXPECT_TRUE(sawSpr) << "No instruction in ETSGLOBAL.foo:void; mapped to the user spread line.";
EXPECT_TRUE(sawLam) << "No instruction in ETSGLOBAL.foo:void; mapped to the user lambda line.";
}
TEST_F(ScopeLineInfoTest, LambdaInvokeGuardUsesIllegalLineNumberInFunction)
{
std::string_view text = R"(function func():void{
let test:()=>void=()=>{
console.log("111");
return;
}
test();
console.log("2222222");
}
)";
std::array args = {
ES2PANDA_BIN_PATH,
"--debug-info=true",
"--opt-level=0",
"--ets-unnamed",
};
auto program = GetCurrentProgramWithArgs({args.data(), args.size()}, text);
ASSERT_NE(program, nullptr);
const auto lines = SplitLines(text);
const auto *func = FindFunctionByName(*program, "ETSGLOBAL.func:void;");
ASSERT_NE(func, nullptr);
const uint32_t callLine = FindLineNo(lines, "test();");
const uint32_t nextUserLine = FindLineNo(lines, R"(console.log("2222222");)");
ASSERT_GT(callLine, 0U);
ASSERT_GT(nextUserLine, 0U);
const auto checkResult = CheckLambdaInvokeGuardLineInfo(*func, callLine, nextUserLine);
EXPECT_TRUE(checkResult.sawLambdaInvoke) <<
"Failed to find the lambda invoke followed by its compiler-generated store and checkcast.";
EXPECT_TRUE(checkResult.sawNextUserLine) << "Failed to find the next user-authored line after the lambda invoke.";
}
TEST_F(ScopeLineInfoCapiTest, NoDebugLineFlagMakesLambdaFunctionInvalid)
{
GTEST_SKIP() << "Temporarily skipped to avoid sanitizer-only leak noise from the CAPI compile path.";
std::string_view text = R"(
function foo() {
let captured = 1;
let lam = () => {
console.log(captured);
};
lam();
}
)";
const auto normalSummary = CompileWithFlag(text, false);
const auto flaggedSummary = CompileWithFlag(text, true);
bool sawFunctionBecomeFullyInvalid = false;
for (const auto &[fnName, allLinesInvalid] : flaggedSummary.functionAllLinesInvalid) {
if (!allLinesInvalid) {
continue;
}
auto it = normalSummary.functionAllLinesInvalid.find(fnName);
if (it != normalSummary.functionAllLinesInvalid.end() && !it->second) {
sawFunctionBecomeFullyInvalid = true;
break;
}
}
EXPECT_TRUE(sawFunctionBecomeFullyInvalid)
<< "Expected at least one lambda-generated function to switch to fully invalid line numbers "
"after AstNodeSetNoDebugLineFlag was applied.";
}
}