基于 AST 与 Proxy沙箱 的局部代码热验证

作者:July_lly日期:2026/3/16

前言

在真实开发中系统中,我们常常会做/需要做一些代码运行或者检测工作。但是全量的代码运行消耗的时间是漫长的。那么我们有没有办法能够只处理我们修改的部分呢?答案是肯定的。

下面将验证介绍一种结合 AST (抽象语法树)沙箱技术 的方案,局部代码热验证。

具体重服务mock代码会放在文章末尾

整体 -> 局部

我们切换一个方向:过去我们总是使用整体运行完拿到export的内容。在一些情况下,不论是 build 构建还是 dev 开发,我们通常都是全量编译打包一次。当然我们可以让他执行两次(比如只测某个函数),不过消耗的时间计算成本将会成倍上升,且容易受到文件中其他无关代码的干扰。

我们不再关注“整个文件”,而是关注 “当前选中的函数及其最小依赖集”。 通过 AST 技术,我们将代码像做手术一样“切”出来,只在内存中构建一个微型的运行环境。

code

先看AST分析转化部分

1import { Node, Project, SyntaxKind } from 'ts-morph';
2
3let lastCodeHash = '';
4
5function extractMinimalUnitForFunction(sourceText: string, functionName: string): { code: string; changed: boolean } {
6    const project = new Project({ useInMemoryFileSystem: true });
7    const sourceFile = project.createSourceFile('heavy-service.ts', sourceText);
8
9    const topLevelDeclMap = new Map<string, Node>();
10
11    for (const stmt of sourceFile.getStatements()) {
12        if (Node.isFunctionDeclaration(stmt) && stmt.getName()) {
13            topLevelDeclMap.set(stmt.getName()!, stmt);
14        }
15        if (Node.isVariableStatement(stmt)) {
16            for (const decl of stmt.getDeclarationList().getDeclarations()) {
17                topLevelDeclMap.set(decl.getName(), stmt);
18            }
19        }
20    }
21
22    if (!topLevelDeclMap.has(functionName)) {
23        throw new Error(`未找到 ${functionName}`);
24    }
25
26    const neededSymbols = new Set<string>([functionName]);
27    const queue = [functionName];
28
29    while (queue.length > 0) {
30        const symbol = queue.shift()!;
31        const declNode = topLevelDeclMap.get(symbol);
32        if (!declNode) continue;
33
34        const ids = declNode.getDescendantsOfKind(SyntaxKind.Identifier);
35        for (const id of ids) {
36            const text = id.getText();
37            if (text === symbol) continue;
38            if (topLevelDeclMap.has(text) && !neededSymbols.has(text)) {
39                neededSymbols.add(text);
40                queue.push(text);
41            }
42        }
43    }
44
45    const allReferencedIds = new Set<string>();
46    for (const sym of neededSymbols) {
47        const node = topLevelDeclMap.get(sym);
48        if (!node) continue;
49        for (const id of node.getDescendantsOfKind(SyntaxKind.Identifier)) {
50            allReferencedIds.add(id.getText());
51        }
52    }
53
54    const importLines: string[] = [];
55    for (const stmt of sourceFile.getStatements()) {
56        if (!Node.isImportDeclaration(stmt)) continue;
57        const usedNames = stmt.getNamedImports()
58            .map((ni) => ni.getName())
59            .filter((n) => allReferencedIds.has(n));
60        if (usedNames.length > 0) {
61            const moduleName = stmt.getModuleSpecifierValue();
62            importLines.push([`import { ${usedNames.join(', ')} } from '${moduleName}';`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.join.md));
63        }
64    }
65
66    const minimalStatements: Node[] = [];
67    for (const stmt of sourceFile.getStatements()) {
68        if (Node.isFunctionDeclaration(stmt) && stmt.getName() && neededSymbols.has(stmt.getName()!)) {
69            minimalStatements.push(stmt);
70            continue;
71        }
72        if (Node.isVariableStatement(stmt)) {
73            const names = stmt.getDeclarationList().getDeclarations().map((d) => d.getName());
74            if (names.some((n) => neededSymbols.has(n))) {
75                minimalStatements.push(stmt);
76            }
77        }
78    }
79
80    const declLines = minimalStatements.map((s) => s.getText());
81    const minimalCode = [...importLines, '', ...declLines].join('\n');
82
83    console.log('--- AST 提取的最小单元 ---\n', minimalCode, '\n--- 结束 ---\n');
84
85    const currentHash = hashCode(minimalCode);
86    const changed = currentHash !== lastCodeHash;
87    lastCodeHash = currentHash;
88
89    return { code: minimalCode, changed };
90}
91

大致描述一下: 首先第一次执行扫描一遍文件,把所有的顶层函数名、变量名作为 Key,对应的 AST 节点作为 Value 存起来。这相当于给整个文件画了一张索引表。通过队列来做递归依赖查找,直到把所有嵌套调用的依赖全部找齐。

找齐了依赖还没完,它还要处理 import,进行treeShaking,最后计算生成的 minimalCode 的哈希值,如果我们改了文件中不相关的部分(比如改了另一个函数),这个最小单元的 Hash 就不会变。只有修改的代码真正影响到了目标函数时,changed 才会是 true

这里面其实牵扯出一个概念:节点回溯

节点回溯

在编译器和代码分析领域,节点回溯(Node Traversal / Upward Walking) 就像是给 AST装上了“导航回程”系统。

如果说传统的 AST 遍历是“从树根向下寻找叶子”,那么节点回溯就是 “从叶子向上寻找祖先”

例如: 我们修改了一个数字 10

  1. 定位: 你的编辑器告诉你,位置在第 500 行,对应 AST 里的 NumericLiteral
  2. 回溯第一步: 它的 parent 是一个 BinaryExpression (例如 x + 10)。
  3. 回溯第二步: 再往上,是一个 VariableDeclarator (例如 const total = x + 10)。
  4. 回溯第三步: 再往上,是一个 BlockStatement(函数体的大括号)。
  5. 回溯终点: 最终碰到 FunctionDeclaration

此时回溯停止。成功锁定:这次修改的影响范围就在函数FunctionDeclaration内。

相关import引用处理

这时候其实我们会发现代码中存在import { round2 } from './tax-utils'这种导入工具的方法,treeShaking也会认为他是真实存在的。而在真实开发中,这个导入可能是非常多的。可能相关的引用缠绕的太深不会比重新构建引用试图,编译一次耗时差多少。

我们可以考虑一下我们这个引用是否是全部真实需要的呢?如果需要我们可以保留编译进我们的文件内,不需要我们是否可以不要这些依赖。

proxy沙箱代理

当我们拿到了相关代码时,不做任何操作进行运行或者是打包其实本身自带的依赖的bundle还是会有很深引用层级,这时候我们可以使用proxy对我们要代理的对象路径进行更改,指定他们或者直接取消引用都是可以,但是为了代码的健壮性与稳定性,我们通常通过proxy进行代理访问。

1 // 定义你的调控配置
2    const config = {
3        // 强制 Mock 的路径模式
4        mockPatterns: ['./tax-utils'],
5        // 即使被引用也不提取源码,直接用 Proxy 占位
6    };
7    const proxyInjections: string[] = [];
8    const finalImportLines: string[] = [];
9
10    // 预设一个万能 Proxy 定义
11    const MAGIC_PROXY_DEF = `const __MAGIC_PROXY__ = new Proxy(() => __MAGIC_PROXY__, {
12        get: (target, prop) => {
13            // 关键:拦截系统转换请求
14            if (prop === Symbol.toPrimitive) return (hint) => (hint === 'number' ? 0 : '转成string了');
15            if (prop === 'toString' || prop === 'valueOf') return () => '走到toString了 ';
16            if (typeof prop === 'symbol') return '无路可走了只能undefined';
17
18            return __MAGIC_PROXY__;
19        },
20        apply: () => __MAGIC_PROXY__
21    });`;
22
23    // 按每句代码读取
24    for (const stmt of sourceFile.getStatements()) {
25        if (!Node.isImportDeclaration(stmt)) continue;
26
27        const modulePath = stmt.getModuleSpecifierValue();
28        const isMock = config.mockPatterns.some(p => modulePath.includes(p));
29
30        if (isMock) {
31            // 如果在 Mock 名单里,将 import 里的变量名全部指向 Proxy
32            const namedImports = stmt.getNamedImports().map(ni => ni.getName());
33            namedImports.forEach(name => {
34                proxyInjections.push(`const ${name} = __MAGIC_PROXY__;`);
35            });
36        } else {
37            // 否则,正常保留(或者递归提取源码)
38            finalImportLines.push(stmt.getText());
39        }
40    }
41
42    const declLines = minimalStatements.map((s) => s.getText());
43    const minimalCode = [
44        MAGIC_PROXY_DEF,       // 1. 注入 Proxy 引擎
45        ...proxyInjections,    // 2. 注入被拦截的变量声明 (const round2 = ...)
46        '',
47        ...finalImportLines,   // 3. 注入真实的 Import (非 Mock 的路径)
48        '',
49        ...declLines           // 4. 注入目标函数及其内部依赖
50    ].join('\n');
51

我采取了 “逻辑截断与指令重定向” 的策略。通过配置化的 依赖调控(Dependency Control) ,系统会对深层或重型的外部依赖进行“漂白”or “替换”:

  • 拦截深层引用:当 AST 扫描到预设的拦截路径(如 ./tax-utils)时,系统会切断递归,不再打包其源码。
  • 注入递归代理(Recursive Proxy) :在生成的代码头部注入一个的万能代理对象 __MAGIC_PROXY__

原理: 无论目标函数如何调用这些被拦截的依赖(如 service.user.get().name),Proxy 都会通过拦截 getapply 陷阱,返回自身以确保链路不崩溃,从而实现逻辑执行的“硬件加速”。

image.png

最终,系统产出一段包含 [代理定义 + 拦截声明 + 真实 Import + 目标函数] 的纯粹代码段。这段代码被注入内存沙箱(如 vm 模块)进行“影子执行”。 这种姿势不仅甩掉了沉重的依赖包袱,更避开了昂贵的重排(Layout)与全量编译过程。

结尾

我们对“局部热验证”方案的探索,本质上是对现代前端工程两大核心思想的深度集成:

  • AST 节点回溯(Node Traversal):语义化的精准 这不仅是 SlideJS 等解析引擎实现精准定位的基础,更是所有现代编译器(Babel, SWC, esbuild)的灵魂。它让我们脱离了低效的正则匹配,进入了“语义化操控”的时代。在本项目中,回溯机制确保了我们能以毫秒级速度,从海量源码中锁定受影响的“逻辑最小单元”。
  • Proxy 沙箱代理:从“物理依赖”到“协议仿真” Proxy 劫持微前端(隔离沙箱)Vue 3(响应式系统) 以及 Vite(依赖预构建拦截) 等基建工具的共同基石。在我们的方案中,它不仅用于隔离,更用于“欺骗”——通过伪造深层依赖的虚幻环境,让局部逻辑在脱离母体后依然能保持强健执行。

这里面之时还是比较干的,可以仔细运行读取一下练习。

1// 重执行函数
2import { normalizeIncome, round2 } from './tax-utils';
3import { test } from './test-utils';
4const serviceName = 'heavy-tax-service';
5
6// 模拟重负载初始化(busy wait)
7function sleepMs(ms: number): void {
8  const start = Date.now();
9  while (Date.now() - start < ms) {
10    // busy wait:模拟数据库连接、缓存预热等耗时操作
11  }
12}
13
14const taxRate = 0.13;
15const extraFee = 12;
16
17/**
18 * 目标函数:我们真正想热验证的逻辑。
19 * 依赖:taxRate、extraFee(本文件声明) + normalizeIncome、round2(来自 ./tax-utils)
20 */
21export function calculateTax(income: number): number {
22  const normalized = normalizeIncome(income);
23  const baseTax = normalized * taxRate + extraFee;
24  return round2(baseTax);
25}
26
27/**
28 * 对比函数:用于演示 AST diff 增量执行
29 * 当修改这个函数时,AST 分析会只执行这个函数及其依赖,跳过 sleepMs 等无关代码
30 */
31export function calculateDiscount(price: number): any {
32  const discountRate = 0.2;
33  const finalPrice = price * (1 - discountRate);
34  return {
35    value: round2(finalPrice),
36    test_value: test, // 来自 test-utils 的依赖,演示 AST 依赖提取
37  };
38}
39
40console.log('[heavy-service] bootstrapping huge runtime...');
41
42// 关键耗时点:全量执行时会在这里阻塞约 2 
43sleepMs(2000);
44calculateTax(1000);
45
46
47const runtimeConfig = {
48  region: process.env.REGION || 'cn',
49  featureFlag: true,
50};
51
52console.log('[heavy-service] side effects done', runtimeConfig, serviceName);
53

thanks


基于 AST 与 Proxy沙箱 的局部代码热验证》 是转载文章,点击查看原文


相关推荐


GPT-5.4 API 上线了,在openClaw龙虾中试试
程序员陆通2026/3/7

突破性的前沿模型,现已全面开放 OpenAI 最新发布的 GPT-5.4 模型现已正式上线 WellAPI 平台!作为 OpenAI 迄今为止最强大的通用模型,GPT-5.4 在推理能力、编程水平和专业文档处理方面实现了质的飞跃,专为复杂专业工作场景打造 。 GPT-5.4 核心特性解析 1. 原生计算机操作能力 GPT-5.4 是 OpenAI 首个具备原生计算机使用能力的通用模型,这标志着 AI 代理(Agent)技术的重大突破。模型能够直接与计算机系统交互,为开发者和智能代理应用开辟了全新


实测UU远程云电脑:堪称游戏党专属“性能王”,游戏全程流畅,好用到出圈
啊阿狸不会拉杆2026/2/27

前言:本地设备性能拉胯,想畅玩《崩坏星穹铁道》《CSGO2》《鸣潮》《原神》?不用花大价钱组装高配电脑,UU远程云电脑直接帮你解决痛点!作为网易旗下主打游戏场景的云电脑工具,它凭借三款不同显卡机型、低延迟优化,稳居云电脑排行榜前列,堪称游戏党专属“性能王”,实测四款热门游戏全程流畅,好用到出圈。         UU远程云电脑核心优势的是精准适配游戏需求,目前推出三款显卡机型——GTX 1660S(入门款)、RTX 3660(主流款)、RTX 4070Ti/5070(旗舰款),按需选择灵活


IoT 平台可编程化:基于 Pydantic Monty 构建工业级智能自动化链路
Lupino2026/2/19

在万物互联的下半场,设备间的简单联动已无法支撑复杂的工业与商业场景。为了打破“配置化逻辑”的瓶颈,我们正式集成了 Pydantic Monty 运行时环境。这一演进赋予了开发者直接在云端编写 Python 脚本的能力,实现了从“被动连接”到“确定性逻辑自主”的跨越。 1. 核心底座:为什么是 Pydantic Monty? 我们选择了由 Pydantic 团队推出的 Monty 作为脚本引擎。它不仅是 Python 的子集,更是为高性能嵌入式场景量身定制的方案: 轻量级沙箱:相比庞大的标准 P


细说日常 Vibe coding 的十宗罪
mCell2026/2/10

同步至个人站点:细说我日常 AI coding 碰到的十个问题 这一年大量 vibe coding,经典翻车现场真的不少。有些是模型习惯问题,有些是 Agent 工具链缺陷,还有些属于“工程现实 vs 最佳实践”的冲突。下面这十个算是我最常遇到、也最容易让人 当场没绷住 的。 1. hardcode:类型系统被你当摆设 是的,很多 TS / Golang 项目,vibe coding 一顿猛改之后,总会冒出一堆 hardcode。 比如判断任务状态: 你会看到它写:taskResult.st


在 Arch Linux 中安装 **Xorg 服务器**
i建模2026/2/1

在 Arch Linux 中安装 Xorg 服务器(即 xorg-server)及相关组件的步骤如下: 一、核心安装命令 1. 安装 Xorg 服务器 sudo pacman -S xorg-server 此命令会安装 Xorg 的核心服务包,包含 X11 协议的实现和基础组件。 2. 安装显卡驱动(必选) 根据显卡类型选择驱动: Intel 集成显卡:sudo pacman -S xf86-video-intel AMD 显卡:sudo pacman -S xf86-video-amdg


Rust 所有权与借用:从堆栈开始建立心智模型
mCell2026/1/23

本文写作时,极大的借鉴了《The Rust Programming Language》(俗称“Rust 圣经”)中相关章节的内容和结构,在此表示感谢。 写 Rust 的第一道坎,不是语法,也不是宏,而是“我明明只是把变量传给你用一下,怎么它就不属于我了?” 这类困惑通常并不奇怪,因为我们习惯了别的语言那套“内存默认有人兜底”的模型,比如 Javascript、Golang 的自动垃圾回收机制。Rust 恰恰相反:它要求你把内存这件事想清楚,然后把规则写进类型系统,交给编译器在编译期强制执行——


WebSocket 在 Spring Boot 中的实战解析:实时通信的技术利器
苏渡苇2026/1/15

WebSocket 在 Spring Boot 中的实战解析:实时通信的技术利器 一、引言:为什么我们需要 WebSocket? 在传统的 Web 应用中,客户端(浏览器)与服务器之间的通信是 请求-响应 模式:客户端发起请求,服务器处理后返回结果。这种模式适用于大多数场景,但在需要 实时双向通信 的场景下(如聊天室、股票行情、在线协作、游戏等),频繁轮询(Polling)或长轮询(Long Polling)会带来高延迟、高开销的问题。 WebSocket 协议应运而生——它提供了一种全双工、低


小迪安全第二十六天
江边鸟2192026/1/6

写好这些配置好相应的数据库内容 发现不足套用模板使用模板框架 <!DOCTYPE html> <html> <head>    <meta charset="UTF-8">    <!-- 页面标题(动态变量) -->    <title>{page_title}</title>    <style>        /* 全局样式 */        body {            font-family: Arial, sans-serif;  /* 设置默认字体


激活函数有什么用?有哪些常用的激活函数?
aicoting2025/12/29

在深度学习中,激活函数(Activation Function)是神经网络的灵魂。它不仅赋予网络非线性能力,还决定了训练的稳定性和模型性能。那么,激活函数到底是什么?为什么我们非用不可?有哪些经典函数?又该如何选择?本文带你全面解析。 所有相关源码示例、流程图、面试八股、模型配置与知识库构建技巧,我也将持续更新在Github:AIHub,欢迎关注收藏! 阅读本文时,请带着这三个问题思考: 什么是激活函数,为什么需要激活函数? 经典的激活函数有哪些? 怎么选择激活函数? 1. 什么是激活函数,


别再死磕扩散模型了,MiniMax新开源揭示:视觉Tokenizer才是下一个金矿
墨风如雪2025/12/20

在AI绘画和视频生成卷到飞起的今天,不管是大厂还是开源社区,大家似乎都陷入了一个怪圈:拼命堆算力去训练更大的Diffusion Transformer(DiT),指望通过增加生成模型的参数来获得更好的画质。 但就在前两天,凭借海螺视频(Hailuo AI)在圈内名声大噪的MiniMax团队,突然开源了一个名为VTP(Visual Tokenizer Pre-training)的项目。看完他们的论文和代码,我不得不说,这帮人可能刚刚掀翻了视觉生成领域的桌子。 他们抛出了一个极其反直觉的结论:如果我

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客