在做软件工程研究时,我们通常会遇到处理代码片段的问题。
最近在研究上遇到一个问题:如何抽取Solidity代码中的contract
和function
内容?
{{{{}}}}
{}()
等本来需要匹配的特殊字符首先我们需要先处理掉代码中的注释部分,因为这部分如果不去除,后面在使用正则解析时会遇到非常多奇奇怪怪的字符。
去除注释方法如下:
function clearCode(sources) {
// merge files
let code = ''
for (const i in sources) code += sources[i].content
// remove commments
code = code.replace(/(\/\*[\s\S]*?\*\/)|((?/g, '')
// remove import
code = code.replace(/import.*;/g, '')
// remove pragma
code = code.replace(/pragma.*;/g, '')
return code.trim()
}
这里实际使用的一行正则是:(\/\*[\s\S]*?\*\/)|((?;
注意下,这里排出了各种可能出现的://
,因为代码中容易出现类似http://, ftp://这类字符串;
另外上述方法删除了Sol文件的前缀信息,包括pragma
编译版本,import
引入合约等。
第二步,我们需要使用base64来mask引号内容,也就是代码中的字符串,防止字符串里的特殊字符影响接下来的正则匹配。
// to mask string with '...', "..."
hashStr(text, flag = true) {
if (flag) {
const reg = /("[^"]*")|('[^']*')/g
text = text.replace(reg, s => `"${this.encode(s)}"`)
} else {
const reg = /"[^"]*"/g
text = text.replace(reg, s => this.decode(s.slice(1).slice(0, -1)))
}
return text
}
encode(text) {
return btoa(unescape(encodeURIComponent(text)))
}
decode(text) {
return decodeURIComponent(escape(atob(text)))
}
函数hashStr
作用是遮掩(mask)掉代码文本中的所有字符串,思路是连带引号一起转换为base64码。
注意,base64前需要使用encode
,这是因为你很可能遇到字符串中是UTF8编码内容,base64默认只支持latin(ASCII),反之解码base64之后,也需要decode出文本。这是一个常见的坑,需要注意⚠️。
hashStr
关键的正则为:
hash时:("[^"]*")|('[^']*')
;
反向hash时:"[^"]*"
反向时仅匹配"
是因为我在base64后,统一用双引号highlight出这些base64,为的是方便反向解码时容易匹配到。
最后,我们可以放心使用如下超长超长的regex进行capture啦,抓取contract
和function
内容吧。
获取class/contract
getContracts(text) {
text = this.hashStr(text)
const reg =
/(^|\s)(contract|interface|library|abstractcontract)\s[^;{}]*{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{[^{}]*})*})*})*})*})*})*})*})*})*}/g
const res = text.match(reg) || []
for (const i in res) res[i] = this.hashStr(res[i].trim(), false)
return res
}
获取functiion
getFunctions(text) {
text = this.hashStr(text)
const reg =
/(^|\s)((function|event)\s|constructor\s*\(.*\))[^{};]*({(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{[^}{]*})*})*})*})*})*})*})*})*}|;)/g
const res = text.match(reg) || []
for (const i in res) res[i] = this.hashStr(res[i].trim(), false)
return res
}
注意,regex本身是不支持多层嵌套的,这里勉强采用手动嵌套的方式来匹配嵌套的花括号。
getContracts
支持10层嵌套,regex是:(^|\s)(contract|interface|library|abstractcontract)\s[^;{}]*{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{(?:[^{}]+|{[^{}]*})*})*})*})*})*})*})*})*})*}
getFunctions
支持9层嵌套,regex是:(^|\s)((function|event)\s|constructor\s*\(.*\))[^{};]*({(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{(?:[^}{]+|{[^}{]*})*})*})*})*})*})*})*})*}|;)
如需超出预设层数,js会出现timeout,可以自行添加层数
同时,为了方便获得函数名,类名,这里还给出一套方法:
nWord(str, n) {
if (typeof n === 'number') {
const m = str.match(new RegExp('^(?:\\w+\\W+){' + n + '}(\\w+)'))
return m && m[1]
} else if (n.length) {
const arr = []
for (const i of n) arr.push(this.nWord(str, i))
return arr
}
},
getContractName(contractCode) {
const words = this.nWord(contractCode, [0, 1, 2])
if (words[0] === 'abstract') return words[2]
else return words[1]
},
getFunctionName(functionCode) {
const words = this.nWord(functionCode, [0, 1])
if (words[0] === 'constructor') return words[0]
else return words[1]
}
nWord
函数可以获取第n个单词,getContractName
获得合约/类名,getFunctionName
获取方法名。
以上所有步骤结合,就可以获取方法,类的内容,名称。
结合echarts插件还可以构建函数树。
开发中,regex常用工具网站:
工具一:可以显示regex的流程规则图
https://www.debuggex.com/
工具二:在线调试regex和需要匹配的样本文件
https://regexr.com/