import $ from 'gogocode' const logger = require('./logger') import { checkExists } from './index' import { ASTNode, EnumTypeNode, HTMLNode } from '../typings/ast.types' export class Ast { /** * 抽象语法树类型枚举 * FunctionDeclaration (函数)、VariableDeclaration(变量) * ImportDeclaration(ts import 引入对象)、CallExpression(表达式) * ExpressionStatement(表达式语句) */ ast: any = null // vue文件专属抽象语法树 htmlAst: any = null jsAst: any = null filePath: string = '' fileType: string = '' // 枚举: html、vue、javascript(自己定义的) enumTypeNode: EnumTypeNode = { FunctionDeclaration: [], VariableDeclaration: [], ImportDeclaration: [], ExportNamedDeclaration: [], Identifier: [], TSInterfaceBody: [] } constructor(filePath: string, option: any, templateString = false) { /** * templateString true 表示 filePath传入的是代码片段(字符串),false表示的代码文件路径地址 * 两者差别会改变语法树解析方式 */ if (templateString) { this.ast = $(filePath, option) } else { if (!this.checkExistsFile(filePath)) return this.filePath = filePath this.ast = $.loadFile(filePath, option) this.fileType = this.ast.parseOptions && this.ast.parseOptions.language || 'javascript' } this.init() } init () { try { switch (this.fileType) { case 'javascript': // 根据抽象语法树类型枚举过滤node this.ast.attr('program.body').forEach((node: any, index: number) => { if (index === 0) { // console.log(node.declaration.declarations[0].id.name, 'node.type'); } if (node.type in this.enumTypeNode) { this.enumTypeNode[node.type].push(node) } }) break case 'vue': this.ast.find('').each((item: any) => { this.jsAst = item }) this.ast.find('').each((item: any) => { this.htmlAst = item }) break case 'html': break } } catch (error) { logger.exit(new Error(`解析抽象语法树失败,请检查需插入的文件是否有语法错误!`)) } } /** * 检查文件是否存在 */ checkExistsFile (filePath: string) { if (!checkExists(filePath, filePath.indexOf('node_modules') > -1 ? false : true)) { logger.exit(new Error(`需注入代码的文件${filePath}缺失!`)) return false } else { return true } } /** * 写入文件 */ writeFile (filePath: string = this.filePath) { let res = $.writeFile(this.ast.generate(), filePath) // logger.success(`${filePath},文件写入成功`) } generateNode (value: any) { let type = Object.prototype.toString.call(value) let init let params = $(`let variable = ${['[object Object]', '[object Array]'].includes(type) ? JSON.stringify(value) : typeof value === 'string' ? `${value}` : value}`).find('variable').each((item: any, index: number) => { init = item[0].nodePath.parentPath.value.init }) return init } /** * 修改对象其中一个字段 * @param {*} obj * @param {*} key * @param {*} value */ editObjectField (objectName: string, key: string, value: any, ast: ASTNode = this.ast) { let node = this.getVariableNode(objectName, ast) let nodeType = this.getNodeType(node) let fieldNode if (node) { if (nodeType === 'CallExpression') { // 此处针对vue中ref定义Object变量进行添加字段,ref本质上是一个函数 fieldNode = node.attr('declarations')[0].init.arguments[0].properties.filter((node: any) => { return node.key.name === key })[0] } else { fieldNode = node.attr('declarations')[0].init.properties.filter((node: any) => { return node.key.name === key })[0] } } if (fieldNode) { fieldNode.value = this.generateNode(value) } } /** * 给对象添加字段 * @param {ast} ast 抽象语法树 * @param {String} objectName 被添加对象 * @param {Object} injectObject 添加的对象 */ addFieldToObject (objectName: string, injectObject: any, ast: ASTNode = this.ast,) { // JSON序列化后生成抽象语法树,再获取到对象的properties let injectObjectAst = $(JSON.stringify(injectObject)) let injectProperties = injectObjectAst.attr('properties') || [] let node = this.getVariableNode(objectName, ast) if (node) { // 此处针对vue中ref定义Object变量进行添加字段,ref本质上是一个函数 let nodeType = this.getNodeType(node) if (nodeType === 'CallExpression') { node.attr('declarations')[0].init.arguments[0].properties = node.attr('declarations')[0].init.arguments[0].properties.concat(injectProperties) } else { node.attr('declarations')[0].init.properties = node.attr('declarations')[0].init.properties.concat(injectProperties) } } } /** * 修改变量的值 * * 当赋值值为string时,添加反单引号,`'value'`,当值为其他的变量名时,'value' * * bug:当赋予的值的类型对象,且存在key的值的类型为Function时,那么该key会被过滤 * @param {string} variable 变量名称 * @param {*} value */ editVariableNode (variable: string, value: any, ast: ASTNode = this.ast) { let node = this.getVariableNode(variable, ast) let nodeType = this.getNodeType(node) if (node) { if (nodeType === 'CallExpression') { node.attr('declarations')[0].init.arguments = [this.generateNode(value)] } else { node.attr('declarations')[0].init = this.generateNode(value) } } } /** * 获取当前抽象节点树下的变量node * @param {*} ast * @param {string||number} variable * @returns {} 返回值 node:'变量节点',value:'变量值',name:'变量名称' */ getVariableNode (variable: string, ast: ASTNode = this.ast) { try { /** * 遍历node节点,找到抽象语法树中的声明变量 * 过滤出变量名称与参数传入一致的节点 * 修改节点的properties */ // program.body 源文件作用域下的写法 ,body.body是函数当前作用域下的写法 let body = ast.attr('program.body') || ast.attr('body.body') let varNode = body.filter((e: any) => { return e.type === 'VariableDeclaration' }) let nodeArray = varNode.filter((e: any) => { return e.declarations[0].id.name === variable }) if (nodeArray.length > 0) { return ast.find(nodeArray[0]) } else { logger.exit(new Error(`未找到节点${variable}!`)) return null } } catch (error) { console.log(error); return null } } /** * 根据函数名获取函数内部node(节点) * * @param {*} ast * @param {*} fnName * @returns */ getFunctoinNode (fnName: string, ast: ASTNode = this.ast) { try { let temp = '' let node = null ast.find(`function ${fnName}() {}`).each((item: any, index: number) => { node = item }) return node } catch (error) { return null } } /** * 弃用 * 根据函数名获取函数抽象语法树 * @param {*} ast * @param {*} fnName * @returns */ getFunctoinAst (fnName: string, ast: ASTNode = this.ast) { try { return $(ast.find(`function ${fnName}() {}`)) } catch (error) { return null } } /** * 根据名称获取函数node * @param fnName * @param ast * @returns */ getCallFunction (fnName: string, ast: ASTNode = this.ast) { try { let node = null ast.find(`${fnName}()`).each((item: any, index: number) => { node = item }) return node } catch (error) { return null } } /** * 同作用域下插入代码片段 * @param {*} code * @param {*} ast */ appendCode (code: string, ast = this.ast) { ast.after(code) } // 插入import insertImport (code: string, ast = this.ast) { // import 末尾插入 if (this.enumTypeNode.ImportDeclaration.length > 0) { let lastImport = this.enumTypeNode.ImportDeclaration[this.enumTypeNode.ImportDeclaration.length - 1] ast.find(lastImport).each((e: any) => { e.after('\n' + code) }) } else { // 文件开头插入 ast.before('\n' + code) } } /** * 删除声明函数 * @param {*} fnName * @param {*} ast */ removeFunction (fnName: string, ast: ASTNode = this.ast) { ast.find(`function ${fnName}() {}`).remove() } /** * 删除声明变量 * @param {*} variableName * @param {*} ast */ removeVariable (variableName: string, ast: ASTNode = this.ast) { ast.find(`let ${variableName} = $_$1`).remove() ast.find(`var ${variableName} = $_$1`).remove() ast.find(`const ${variableName} = $_$1`).remove() } /** * 删除节点,返回根节点 * @param node */ removeNode (node: any, ast: ASTNode = this.ast) { ast.find(node).remove() } /** * 获取节点类型 * @param node * @returns */ getNodeType (node: ASTNode) { if (node) { return node.attr('declarations')[0].init.type } } /** * 根据export的变量名称找到对应的node * @param variableName * @returns * @description 目前实例只支持常量 */ getExportNode (variableName: string) { let resultNode: any this.enumTypeNode['ExportNamedDeclaration'].forEach((node: any) => { try { if (node.declaration.declarations[0].id.name === variableName) { resultNode = node } } catch (error) { } }) if (resultNode) { return resultNode } else { logger.error('没有找到export对象') } } /** * 生成interface Node * generateInterfaceNode * @param {*} name */ generateIdentifierNode (name: any, hasT = false, comment = '') { let template = hasT ? $(` //注释占位符 export interface 占位符 { } `) : $(` //注释占位符 export interface 占位符 { } `) let node = template.attr('program').body[0] //注释 if (comment) { node.leadingComments[0].value = comment node.comments[0].value = comment } else { delete node.leadingComments delete node.comments } let declaration = node.declaration declaration.id = name return node /** * interfacePanel.attr('program').body[0].id 最外层 interface名称 * nterfacePanel.attr('program').body[0].body interface的字段配置 * 字段配置相关的 名称、类型、注释 * 注释相关的字段为 leadingComments、comments */ } // interface 字段类型 generateTSTypeAnnotationNode (keyName: any, keyType: any, fieldConfig: any, businessName: any, required: string[] = []) { let { items, example } = fieldConfig // 字段类型是数组的情况下回有items,里面是数组集合的类型 let itemsType: string = 'any' if (keyType === 'array') { const TYPE_MAP: any = { integer: 'number', string: 'string', boolean: 'boolean', number: 'number', array: 'array' } // 如果是实体类,又将生成一个 if ('originalRef' in items) { itemsType = this.definitionInterfaceName(items.originalRef, 1) } else { itemsType = TYPE_MAP[items.type] || 'any' } } let template = $(` interface customName { //注释占位符 string: string //注释占位符 number: number //注释占位符 boolean: boolean //注释占位符 any:any //注释占位符 array:Array<${itemsType}> //注释占位符 array_string:Array //注释占位符 array_interface:Array //注释占位符 choosable?:any //注释占位符 t: T //注释占位符 list:${businessName}List } `) // 字段类型 let mapType = '' switch (keyType) { case 'number': mapType = 'number' break; case 'integer': mapType = 'number' break case 'string': mapType = 'string' break; case 'boolean': mapType = 'boolean' break case 'array': mapType = 'array' break case 'list': // 列表字段类型 名称为业务表示名称 + List mapType = 'list' break default: mapType = 't' break; } let fieldNodes: any try { fieldNodes = template.attr('program').body[0].body.body } catch (error) { return null } let filterNode = fieldNodes.filter((node: { key: { name: string } }) => { return node.key.name === mapType })[0] if (required.includes(keyName)) { filterNode.optional = false } else { filterNode.optional = true } // 字段名称 filterNode.key.name = keyName // 注释 if (example) { filterNode.leadingComments[0].value = example filterNode.comments[0].value = example } else { delete filterNode.leadingComments delete filterNode.comments } return filterNode } /** * 检查interface是否在AST中生成过 * @param interfaceName */ checkedInterfaceExist (interfaceName: string, ast: Ast) { let body = ast.ast.attr('program').body let ids = body.map(node => { return node.declaration.id }) || [] return ids.includes(interfaceName) } /** * 定义interface名称结尾后缀 * @param apiName * @param endingType * @returns */ definitionInterfaceName (apiName: string, endingType: number) { let endingTypeMap: any = { 1: 'Ro', 2: 'Param', } let sliptIndex = apiName.lastIndexOf('/') let interfaceName = apiName.slice(sliptIndex + 1, apiName.length) // 首先改为首字母大写 interfaceName = (interfaceName as string).replace(interfaceName[0], interfaceName[0].toUpperCase()); // 如果接口名称结尾包含Result,则删除 let rLastIndex = interfaceName.lastIndexOf('Result') if (rLastIndex > -1) interfaceName = interfaceName.slice(0, rLastIndex) let filterLetter = [...(String(interfaceName))].filter((e: string) => { return e.charCodeAt(0) > 64 && e.charCodeAt(0) < 91 }) interfaceName = filterLetter.join('').toLowerCase() interfaceName = (interfaceName as string).replace(interfaceName[0], interfaceName[0].toUpperCase()); return interfaceName + endingTypeMap[endingType] } /** * 查看 * @param tagName * @param node */ getElementByTagName (tagName: string, ast: ASTNode = this.htmlAst): ASTNode[] { let tagNodes: ASTNode[] = [] ast.find(`<${tagName}>`).each((node: any) => { tagNodes.push(node) }) // node 为 下的node // if (node.content.children) { // node.content.children.forEach((nodeItem: HTMLNode) => { // if (nodeItem.content.name === tagName) { // tagNodes.push(nodeItem) // } // if (nodeItem.nodeType === 'tag') { // let deepNodes = this.getElementByTagName(tagName, nodeItem) // tagNodes = tagNodes.concat(deepNodes) // } // }) // } return tagNodes } /** * HTML 版本的remove Node,这个方法是通过下标找到具体的node,再从父级元素中剔除 * @param node * @param ast */ removeHtmlNode (node: HTMLNode, ast: ASTNode = this.htmlAst) { let index = node.parentRef.content.children.findIndex((childrenNode: HTMLNode) => { return childrenNode.nodeType === node.nodeType && node.content.name === childrenNode.content.name && node.content.openEnd.endPosition === childrenNode.content.openEnd.endPosition }) if (index > 0) { node.parentRef.content.children.splice(index, 1) } } createHtmlNode () { } /** * 获取DOM元素的属性集合 * @param node * @returns */ getAttributes (node: ASTNode) { const attributes = node.attr('content.attributes') return attributes } /** * 根据Key值,获取DOM元素属性 * @param node * @param key * @returns */ getSingleAttribute (node: ASTNode, key: string) { let attributes = this.getAttributes(node) let value = '' attributes.map((item: any) => { if (item.key.content === key || item.key.content === `${key}\r`) { if (item.value) { value = item.value.content } } }) return value; } /** * 修改DOM元素的属性 * @param node * @param key * @param value */ editAttributes (node: ASTNode, key: string, value: string) { let attributes = this.getAttributes(node) attributes.map((item: any) => { if (item.key.content === key || item.key.content === `${key}\r`) { if (!item.value) { // 这里去掉一个换行,去不去应该都行,新文件保存都会执行一次格式化 item.key.content = item.key.content.replace('\r', '') item.startWrapper = { type: 'token:attribute-value-wrapper-start', content: '"', startPosition: item.key.startPosition, endPosition: item.key.endPosition } item.value = { type: 'token:attribute-value', content: value, startPosition: item.key.startPosition, endPosition: item.key.endPosition } item.endWrapper = { type: 'token:attribute-value-wrapper-end', content: '"', startPosition: item.key.startPosition, endPosition: item.key.endPosition } } else { item.value.content = value } } return item }) } /** * 给DOM元素添加属性,key:value 格式 * @param node * @param key * @param value */ addAttributes (node: ASTNode, key: string, value: any) { let attributes = this.getAttributes(node) let newAttribute = { key: { type: 'token:attribute-key', content: key, }, startWrapper: { type: 'token:attribute-value-wrapper-start', content: '"', }, value: { type: 'token:attribute-value', content: value, }, endWrapper: { type: 'token:attribute-value-wrapper-end', content: '"', } } attributes.push(newAttribute) } /** * 根据传入的key值,移除DOM元素的属性 * @param node * @param key * @param value */ removeAttributes (node: ASTNode, key: string) { let attributes = this.getAttributes(node) let index = attributes.findIndex((item: any) => { return item.key.content === key || item.key.content === `${key}\r` }) if (index > -1) { attributes.splice(index, 1) } } }