#!/usr/bin/env node 'use strict' /** * 原生安卓端 × 云对象契约校验。 * * android/ 是独立 Kotlin 工程,通过 uniCloud「URL 化」HTTP 网关调用云对象: * POST {BASE_URL}/{云对象}/{方法} * 它与前端页面一样存在「方法名写错只在运行时炸」的风险,但不在 * tools/check-cloud-methods.js 的扫描范围(那个扫的是 .vue/.js 前端调用点)。 * * 本脚本从 Android 源码里抽出所有 (云对象, 方法) 调用对, * 与云对象实现交叉比对,输出缺失清单。 * * 用法: * node tools/check-android-contract.js * node tools/check-android-contract.js --json */ const fs = require('fs') const path = require('path') const { collectCloudObjects } = require('./check-cloud-methods') const ROOT = path.resolve(__dirname, '..') const ANDROID_SRC = path.join(ROOT, 'android', 'app', 'src', 'main', 'java') // 调用形式(Kotlin): // api.callChecked("uni-cms-articles", "getPublishedArticles", ...) // api.callRaw("article_info", "getDetail", ...) // 云对象名以 kebab-case、方法名以 snake/camel 出现,连字符是硬特征。 const CALL_RE = /call(?:Checked|Raw)?\s*\(\s*"([\w-]+)"\s*,\s*"([A-Za-z_][\w]*)"/g function walk(dir, acc) { let entries try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return acc } for (const e of entries) { const p = path.join(dir, e.name) if (e.isDirectory()) walk(p, acc) else if (e.name.endsWith('.kt')) acc.push(p) } return acc } function run(opts = {}) { if (!fs.existsSync(ANDROID_SRC)) { const r = { ok: true, skipped: true, reason: '未找到 android/ 原生工程,跳过', errors: [], warnings: [] } if (!opts.json) console.log('安卓端契约校验:未找到 android/ 原生工程,跳过') return r } const { cloudObjects } = collectCloudObjects() const files = walk(ANDROID_SRC, []) const errors = [] const checked = new Set() let callSiteCount = 0 for (const file of files) { const src = fs.readFileSync(file, 'utf8') const rel = path.relative(ROOT, file).replace(/\\/g, '/') let m CALL_RE.lastIndex = 0 while ((m = CALL_RE.exec(src))) { const obj = m[1] const method = m[2] // 跳过非云对象命名(Kotlin/JS 标准 API 里带连字符的极少,这里以云对象表为准) callSiteCount++ const key = `${obj}.${method}` if (checked.has(key)) continue checked.add(key) const line = src.slice(0, m.index).split('\n').length const decl = cloudObjects.get(obj) if (!decl) { // 只报"看起来像本项目云对象"的(存在于 cloudfunctions 命名习惯里) errors.push({ file: rel, line, key, msg: `云对象 ${obj} 未找到实现` }) } else if (!decl.methods.has(method)) { errors.push({ file: rel, line, key, msg: `${key}() 未在 ${decl.file} 中定义` }) } } } const result = { ok: errors.length === 0, androidFileCount: files.length, callSiteCount, uniqueCallCount: checked.size, errors, warnings: [] } if (!opts.json) { console.log('安卓端 × 云对象契约校验') console.log(` Kotlin 文件 ${result.androidFileCount} 个 · 调用点 ${callSiteCount} 处 · 去重 ${checked.size} 个`) if (errors.length) { console.log(`\n✗ 发现 ${errors.length} 个问题:`) for (const e of errors) console.log(` ✗ ${e.file}:${e.line} ${e.msg}`) } else { console.log('\n✓ 安卓端调用的所有云对象方法均存在') } console.log(`\n结论: ${errors.length} 错误 / 0 警告`) } return result } if (require.main === module) { const json = process.argv.includes('--json') const r = run({ json }) if (json) console.log(JSON.stringify(r, null, 2)) process.exit(r.ok ? 0 : 1) } module.exports = { run }