autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
283 lines (282 loc) • 9.27 kB
JavaScript
/**
* @module RubyDslParser
* @description Ruby DSL 轻量解析器 — 从 Boxfile / podspec 类文件中提取项目结构信息
*
* 不需要完整的 Ruby 解析器,使用正则 + 上下文状态机提取:
* - 层级 (layer) 声明与层间访问规则
* - 模块 (box) 声明(本地/远程)
* - 宿主应用信息
* - 模块级 spec 依赖/源文件路径
*
* 支持 EasyBox (Boxfile + *.boxspec) 和结构类似的自研工具。
*/
// ── Boxfile 解析 ────────────────────────────────────
/**
* 解析 Boxfile 内容,提取层级、模块、宿主应用信息
*/
export function parseBoxfile(content) {
const result = {
layers: [],
globalDependencies: [],
};
// 提取 host_app
const hostAppMatch = content.match(/host_app\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?/);
if (hostAppMatch) {
result.hostApp = {
name: hostAppMatch[1],
version: hostAppMatch[2] || '0.0.0',
};
}
// 按 layer block 分割解析
const layerBlocks = extractLayerBlocks(content);
let layerOrder = 0;
for (const block of layerBlocks) {
const layer = {
name: block.name,
order: layerOrder++,
accessibleLayers: extractAccessLayers(block.body),
modules: extractModules(block.body),
};
result.layers.push(layer);
}
// 提取 layer 外层的全局模块声明
const outsideLayerContent = removeLayerBlocks(content);
result.globalDependencies = extractModules(outsideLayerContent);
return result;
}
// ── *.boxspec / *.podspec 解析 ────────────────────────
/**
* 解析 boxspec/podspec 文件内容,提取模块元数据
*/
export function parseModuleSpec(content) {
return {
name: extractSpecField(content, 'name') || 'unknown',
version: extractSpecField(content, 'version') || '0.0.0',
sources: extractSpecField(content, 'source_files') ||
extractSpecField(content, 'sources') ||
extractSpecField(content, 'source') ||
'',
dependencies: extractSpecDependencies(content),
publicHeaders: extractSpecArrayField(content, 'public_headers') ||
extractSpecArrayField(content, 'public_header_files') ||
[],
deploymentTarget: extractSpecDeploymentTarget(content),
};
}
/**
* 提取所有 layer 'Name' do ... end 块
* 使用 do/end 嵌套计数处理嵌套 block
*/
function extractLayerBlocks(content) {
const blocks = [];
const layerRe = /layer\s+['"](\w+)['"]\s+do\b/g;
let match;
while ((match = layerRe.exec(content)) !== null) {
const name = match[1];
const startIndex = match.index;
const bodyStart = match.index + match[0].length;
const endIndex = findMatchingEnd(content, bodyStart);
if (endIndex === -1) {
continue;
}
blocks.push({
name,
body: content.substring(bodyStart, endIndex),
startIndex,
endIndex: endIndex + 3, // 'end' is 3 chars
});
}
return blocks;
}
/**
* 从 do 之后的位置开始,找到匹配的 end
* 处理嵌套的 do...end 块(如 group do ... end)
*/
function findMatchingEnd(content, startPos) {
let depth = 1;
// 逐行扫描以正确识别 do/end 关键字(避免匹配字符串内的)
const lines = content.substring(startPos).split('\n');
let pos = startPos;
for (const line of lines) {
const trimmed = line.trim();
// 跳过注释行
if (trimmed.startsWith('#')) {
pos += line.length + 1;
continue;
}
// 计算该行的 do 和 end
// 匹配行尾的 do(不匹配字符串内的)
if (/\bdo\b\s*(?:#.*)?$/.test(trimmed)) {
depth++;
}
// 匹配行首的 end(独立的 end 关键字)
if (/^\s*end\b/.test(line)) {
depth--;
if (depth === 0) {
return pos;
}
}
pos += line.length + 1;
}
return -1;
}
/**
* 移除所有 layer 块,返回剩余内容(用于提取全局模块)
*/
function removeLayerBlocks(content) {
const blocks = extractLayerBlocks(content);
if (blocks.length === 0) {
return content;
}
let result = '';
let lastEnd = 0;
for (const block of blocks) {
result += content.substring(lastEnd, block.startIndex);
lastEnd = block.endIndex;
}
result += content.substring(lastEnd);
return result;
}
// ── Box/模块提取 ────────────────────────────────────
/**
* 从内容中提取所有 box 声明
*
* 支持格式:
* box 'Name', 'Version'
* box 'Name', :path => 'LocalModule/Name'
* box 'Name', path: 'LocalModule/Name'
* box 'Name', '~> 1.0', :path => '...'
*/
function extractModules(content) {
const modules = [];
const seen = new Set();
// 当前 group 上下文跟踪
let currentGroup;
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// 跳过注释
if (trimmed.startsWith('#')) {
continue;
}
// 检查 group 开始
const groupMatch = trimmed.match(/group\s+['"]([^'"]+)['"]\s+do/);
if (groupMatch) {
currentGroup = groupMatch[1];
continue;
}
// 检查 group/layer 结束
if (/^\s*end\b/.test(line) && currentGroup) {
currentGroup = undefined;
continue;
}
// 解析 box 声明
const boxMatch = trimmed.match(/^box\s+['"]([^'"]+)['"]/);
if (!boxMatch) {
continue;
}
const name = boxMatch[1];
if (seen.has(name)) {
continue;
}
seen.add(name);
const rest = trimmed.substring(boxMatch[0].length);
// 检查是否有 :path(本地模块)
const pathMatch = rest.match(/:path\s*=>\s*['"]([^'"]+)['"]|path:\s*['"]([^'"]+)['"]/);
const isLocal = pathMatch !== null;
const localPath = pathMatch ? pathMatch[1] || pathMatch[2] : undefined;
// 提取版本号
let version = '';
const versionMatch = rest.match(/,\s*['"]([^'"]+)['"]/);
if (versionMatch && !versionMatch[1].includes('/')) {
version = versionMatch[1];
}
modules.push({
name,
version,
isLocal,
localPath,
group: currentGroup,
});
}
return modules;
}
// ── Access 规则提取 ─────────────────────────────────
/**
* 提取 access 声明中的层名列表
*
* 支持格式:
* access 'Layer1', 'Layer2', 'Layer3'
* access "Layer1", "Layer2"
*/
function extractAccessLayers(content) {
const layers = [];
const accessRe = /access\s+(.+)/g;
let match;
while ((match = accessRe.exec(content)) !== null) {
const rest = match[1];
const nameRe = /['"]([^'"]+)['"]/g;
let nameMatch;
while ((nameMatch = nameRe.exec(rest)) !== null) {
if (!layers.includes(nameMatch[1])) {
layers.push(nameMatch[1]);
}
}
}
return layers;
}
// ── Spec 字段提取 ───────────────────────────────────
/**
* 从 podspec/boxspec 中提取单值字段
* 支持: s.name = 'Value' 和 spec.name = 'Value'
*/
function extractSpecField(content, field) {
const re = new RegExp(`\\b\\w+\\.${field}\\s*=\\s*['"]([^'"]+)['"]`, 'i');
const match = content.match(re);
return match ? match[1] : undefined;
}
/**
* 从 podspec/boxspec 中提取 dependency 声明列表
*
* 支持:
* s.dependency 'ModuleName'
* s.dependency 'ModuleName', '~> 1.0'
* s.dependency "ModuleName"
*/
function extractSpecDependencies(content) {
const deps = [];
const re = /\b\w+\.dependency\s+['"]([^'"]+)['"]/g;
let match;
while ((match = re.exec(content)) !== null) {
if (!deps.includes(match[1])) {
deps.push(match[1]);
}
}
return deps;
}
/**
* 从 podspec/boxspec 中提取数组字段
* 支持: s.public_headers = ['path1', 'path2']
*/
function extractSpecArrayField(content, field) {
const re = new RegExp(`\\b\\w+\\.${field}\\s*=\\s*\\[([^\\]]+)\\]`, 'i');
const match = content.match(re);
if (!match) {
return undefined;
}
const items = [];
const itemRe = /['"]([^'"]+)['"]/g;
let m;
while ((m = itemRe.exec(match[1])) !== null) {
items.push(m[1]);
}
return items.length > 0 ? items : undefined;
}
/**
* 提取部署目标版本
* 支持: s.ios.deployment_target = '13.0'
*/
function extractSpecDeploymentTarget(content) {
const match = content.match(/\b\w+\.ios\.deployment_target\s*=\s*['"]([^'"]+)['"]/);
return match ? match[1] : undefined;
}