🔍 我开源了 `miniaudit`:一个基于 AST 的微信小程序审核预检 CLI,用静态分析提前拦截 13+ 高频提审雷区

54 阅读2分钟

“又双叒叕被拒了?就因为 onLoad 里调了个 wx.getUserProfile?”

作为长期维护小程序的开发者,我深知微信审核规则的“模糊性”和“滞后性”。人工排查成本高、易遗漏,而官方又不提供本地校验工具。

于是,我用 Node.js + Babel Parser(AST)+ 静态分析,打造了 miniaudit —— 一个专为小程序提审设计的本地预检 CLI 工具

🧠 技术亮点

  • 精准 AST 分析:不只是字符串匹配!
    能识别 onLaunch/onLoad/onShow 中的函数调用链,避免误报
  • 多文件协同检测
    结合 app.json(权限声明)、WXML(默认勾选)、JS(API 调用)综合判断
  • 零依赖、高性能
    10,000 行代码 < 2 秒,不污染项目依赖
  • CI/CD 友好
    支持 --format json,轻松集成到 GitHub Actions

🛠️ 核心规则示例(R12:进入即授权)

// lib/rules/rule12-immediate-auth-on-launch.js
const traverse = require('@babel/traverse').default;

module.exports = {
    id: 'R12',
    async check({ jsFiles, parseJS }) {
        const sensitiveApis = new Set(['login', 'getUserProfile', 'getPhoneNumber']);
        const entryMethods = new Set(['onLaunch', 'onLoad', 'onShow']);

        for (const file of jsFiles) {
            try {
                const ast = parseJS(file);
                let found = null;

                // 遍历所有 CallExpression,找 App 或 Page
                traverse(ast, {
                    CallExpression(path) {
                        if (found) return; // 已找到,提前退出

                        const callee = path.get('callee');
                        let targetName = null;

                        if (callee.isIdentifier({ name: 'App' })) {
                            targetName = 'App';
                        } else if (callee.isIdentifier({ name: 'Page' })) {
                            targetName = 'Page';
                        }

                        if (targetName) {

                            if (path.node.arguments.length === 0) {
                                return;
                            }

                            const configArg = path.get('arguments')[0];
                            if (!configArg.isObjectExpression()) {
                                return;
                            }

                            const props = configArg.get('properties');

                            if (!Array.isArray(props)) {
                                return;
                            }

                            for (const propPath of props) {
                                let methodName = '';
                                let valuePath = null;

                                // ✅ 支持 ObjectMethod (onLaunch() {})
                                if (propPath.isObjectMethod()) {
                                    const keyNode = propPath.node.key;
                                    if (keyNode.type === 'Identifier') {
                                        methodName = keyNode.name;
                                    } else {
                                        continue;
                                    }
                                    valuePath = propPath.get('body'); // ObjectMethod 的函数体

                                    // ✅ 支持 ObjectProperty (onLaunch: function() {})
                                } else if (propPath.isObjectProperty()) {
                                    const keyNode = propPath.node.key;
                                    if (keyNode.type === 'Identifier') {
                                        methodName = keyNode.name;
                                    } else if (keyNode.type === 'StringLiteral') {
                                        methodName = keyNode.value;
                                    } else {
                                        continue;
                                    }
                                    valuePath = propPath.get('value');

                                } else {
                                    continue;
                                }

                                // 检查是否是入口方法
                                if (!entryMethods.has(methodName)) {
                                    continue;
                                }

                                // 确保 value 是函数体(ObjectMethod 的 body 是 BlockStatement)
                                if (!valuePath || !valuePath.isBlockStatement && !valuePath.isFunction()) {
                                    continue;
                                }


                                // 在函数体内查找 wx. 敏感调用
                                valuePath.traverse({
                                    CallExpression(innerPath) {
                                        if (found) return;

                                        const innerCallee = innerPath.get('callee');
                                        if (
                                            innerCallee.isMemberExpression() &&
                                            innerCallee.get('object').isIdentifier({ name: 'wx' }) &&
                                            innerCallee.get('property').isIdentifier()
                                        ) {
                                            const apiName = innerCallee.node.property.name;

                                            if (sensitiveApis.has(apiName)) {
                                                found = {
                                                    file,
                                                    line: innerPath.node.loc?.start.line || '?',
                                                    api: apiName,
                                                    method: methodName
                                                };
                                                innerPath.stop();
                                            } else {
                                            }
                                        }
                                    }
                                });
                            }
                        }
                    }
                });

                if (found) {

                    return {
                        level: 'error',
                        message: `在 ${found.method} 中立即调用 wx.${found.api}(未先提供功能体验)`,
                        location: `${found.file}:${found.line}`,
                        suggestion: '请先让用户浏览/使用核心功能,再在用户主动操作时请求授权'
                    };
                } else {
                }
            } catch (e) {
            }
        }

        return { level: 'pass', message: '未发现启动时立即授权' };
    }
};

### ▶️ 快速体验
```bash
npx miniaudit ./my-miniprogram --fix
  • 自动注释 console.log / debugger
  • 输出彩色报告,高危项标红

🤖 CI 集成示例(GitHub Actions)

- name: Run miniaudit
  run: npx miniaudit .
  # 若发现高危问题,exit code ≠ 0,自动阻断合并

🌈 开源共建

💬 如果你有更多审核雷区经验,欢迎 PR 新规则!让社区一起告别“提审焦虑”。