dsa-connectors/status-checks/check.js
Ardhead 0ef4667258
Checker for solidity code (#32)
* raw logic

* rename and output update

* checks update

* name & comments checks update

* Updated comments for reflexer

* Updated comments

* colors and line numbers

* commit test

* commit test 2

* commit test 3

* commit test 4

* commit test 5

* commit test 6

* commit test 7

* reset branch

* commit test 1

* commit test 1

* test commit 1

* test commit

* test

* test

* test workflow

Co-authored-by: Thrilok Kumar <thrilok2000@gmail.com>
2021-05-07 22:06:52 +05:30

317 lines
11 KiB
JavaScript

const fs = require('fs')
const path = require('path')
const forbiddenStrings = ['selfdestruct']
const getConnectorsList = async () => {
try {
const connectors = []
const connectorsRootsDirs = ['mainnet', 'polygon']
for (let index = 0; index < connectorsRootsDirs.length; index++) {
const root = `contracts/${connectorsRootsDirs[index]}/connectors`
const dirs = [root]
while (dirs.length) {
const currentDir = dirs.pop()
const subs = fs.readdirSync(currentDir, { withFileTypes: true })
for (let index = 0; index < subs.length; index++) {
const sub = subs[index]
if (sub.isFile() && sub.name === 'main.sol') {
connectors.push(currentDir)
} else if (sub.isDirectory()) {
dirs.push(`${currentDir}/${sub.name}`)
}
}
}
}
return connectors.map(dir => ({ path: dir }))
} catch (error) {
return Promise.reject(error)
}
}
const checkCodeForbidden = async (code, codePath) => {
try {
const forbidden = []
for (let i1 = 0; i1 < forbiddenStrings.length; i1++) {
const forbiddenStr = forbiddenStrings[i1]
const strs = code.split('\n')
for (let i2 = 0; i2 < strs.length; i2++) {
if (strs[i2].includes(forbiddenStr)) {
forbidden.push(`found '${forbiddenStr}' in ${codePath}:${i2 + 1}`)
}
}
}
return forbidden
} catch (error) {
return Promise.reject(error)
}
}
const checkForbidden = async (parentPath, codePath = './main.sol') => {
try {
if (codePath.startsWith('@')) {
codePath = path.resolve('node_modules', `./${codePath}`)
} else {
codePath = path.resolve(parentPath, codePath)
}
const code = fs.readFileSync(codePath, { encoding: 'utf8' })
const forbidden = await checkCodeForbidden(code, codePath)
if (code.includes('import')) {
const importsPathes = code
.split('\n')
.filter(str => str.includes('import') && str.includes('from') && str.includes('.sol'))
.map(str => str.split('from')[1].replace(/["; ]/gi, ''))
for (let index = 0; index < importsPathes.length; index++) {
const forbiddenErrors = await checkForbidden(
path.parse(codePath).dir,
importsPathes[index]
)
forbidden.push(...forbiddenErrors)
}
}
return codePath.endsWith('main.sol') ? { forbiddenErrors: forbidden, code } : forbidden
} catch (error) {
return Promise.reject(error)
}
}
const checkEvents = async (connector) => {
try {
const errors = []
const warnings = []
const eventsPath = `${connector.path}/events.sol`
const mainPath = `${connector.path}/main.sol`
if (connector.events.length) {
const eventNames = []
for (let i1 = 0; i1 < connector.mainEvents.length; i1++) {
const mainEvent = connector.mainEvents[i1]
const name = mainEvent.split('(')[0]
eventNames.push(name)
const event = connector.events.find(e => e.split('(')[0].split(' ')[1] === name)
if (event) {
const mainEventArgs = mainEvent.split('(')[1].split(')')[0].split(',').map(a => a.trim())
const eventArgs = event.split('(')[1].split(')')[0].split(',').map(a => a.trim())
if (mainEventArgs.length !== eventArgs.length) {
errors.push(`arguments amount don't match for ${name} at ${mainPath}:${connector.mainEventsLines[i1]}`)
continue
}
for (let i2 = 0; i2 < mainEventArgs.length; i2++) {
if (!mainEventArgs[i2].startsWith(eventArgs[i2].split(' ')[0])) {
errors.push(`invalid argument #${i2 + 1} for ${name} at ${mainPath}:${connector.mainEventsLines[i1]}`)
}
}
} else {
errors.push(`event ${name} missing at ${eventsPath}`)
}
}
if (connector.mainEvents.length < connector.events.length) {
const deprecatedEvents = connector.events.filter(e => {
let used = false
for (let index = 0; index < eventNames.length; index++) {
if (e.split('(')[0].split(' ')[1] === eventNames[index]) used = true
}
return !used
})
warnings.push(`${deprecatedEvents.map(e => e.split('(')[0].split(' ')[1]).join(', ')} event(s) not used at ${connector.path}/main.sol`)
}
} else {
warnings.push(`missing events file for ${connector.path}/main.sol`)
}
return { eventsErrors: errors, eventsWarnings: warnings }
} catch (error) {
return Promise.reject(error)
}
}
const getCommments = async (strs) => {
try {
const comments = []
let type
for (let index = strs.length - 1; index >= 0; index--) {
const str = strs[index]
if (!type) {
if (str.trim().startsWith('//')) {
type = 'single'
} else if (str.trim().startsWith('*/')) {
type = 'multiple'
}
}
if (type === 'single' && str.trim().startsWith('//')) {
comments.push(str.replace(/[/]/gi, '').trim())
} else if (type === 'multiple' && !str.trim().startsWith('/**') && !str.trim().startsWith('*/')) {
comments.push(str.replace(/[*]/gi, '').trim())
} else if (type === 'single' && !str.trim().startsWith('//')) {
break
} else if (type === 'multiple' && str.trim().startsWith('/**')) {
break
}
}
return comments
} catch (error) {
return Promise.reject(error)
}
}
const parseCode = async (connector) => {
try {
const strs = connector.code.split('\n')
const events = []
const eventsFirstLines = []
let func = []
let funcs = []
let event = []
let mainEvents = []
let firstLine
let mainEventsLines = []
for (let index = 0; index < strs.length; index++) {
const str = strs[index]
if (str.includes('function') && !str.trim().startsWith('//')) {
func = [str]
firstLine = index + 1
} else if (func.length && !str.trim().startsWith('//')) {
func.push(str)
}
if (func.length && str.startsWith(`${func[0].split('function')[0]}}`)) {
funcs.push({
raw: func.map(str => str.trim()).join(' '),
comments: await getCommments(strs.slice(0, firstLine)),
firstLine
})
func = []
}
}
funcs = funcs
.filter(({ raw }) => {
if ((raw.includes('external') || raw.includes('public')) &&
raw.includes('returns')) {
const returns = raw.split('returns')[1].split('(')[1].split(')')[0]
return returns.includes('string') && returns.includes('bytes')
}
return false
})
.map(f => {
const args = f.raw.split('(')[1].split(')')[0].split(',')
.map(arg => arg.trim())
.filter(arg => arg !== '')
const name = f.raw.split('(')[0].split('function')[1].trim()
return {
...f,
args,
name
}
})
const eventsPath = `${connector.path}/events.sol`
if (fs.existsSync(eventsPath)) {
mainEvents = funcs
.map(({ raw }) => raw.split('_eventName')[2].trim().split('"')[1])
.filter(raw => !!raw)
mainEventsLines = mainEvents.map(me => strs.findIndex(str => str.includes(me)) + 1)
const eventsCode = fs.readFileSync(eventsPath, { encoding: 'utf8' })
const eventsStrs = eventsCode.split('\n')
for (let index = 0; index < eventsStrs.length; index++) {
const str = eventsStrs[index]
if (str.includes('event')) {
event = [str]
firstLine = index + 1
} else if (event.length && !str.trim().startsWith('//')) {
event.push(str)
}
if (event.length && str.includes(')')) {
events.push(event.map(str => str.trim()).join(' '))
eventsFirstLines.push(firstLine)
event = []
}
}
}
return {
...connector,
events,
eventsFirstLines,
mainEvents,
mainEventsLines,
funcs
}
} catch (error) {
return Promise.reject(error)
}
}
const checkComments = async (connector) => {
try {
const errors = []
for (let i1 = 0; i1 < connector.funcs.length; i1++) {
const func = connector.funcs[i1]
for (let i2 = 0; i2 < func.args.length; i2++) {
const argName = func.args[i2].split(' ').pop()
if (!func.comments.some(
comment => comment.startsWith('@param') && comment.split(' ')[1] === argName
)) {
errors.push(`argument ${argName} has no @param for function ${func.name} at ${connector.path}/main.sol:${func.firstLine}`)
}
}
const reqs = ['@dev', '@notice']
for (let i3 = 0; i3 < reqs.length; i3++) {
if (!func.comments.some(comment => comment.startsWith(reqs[i3]))) {
errors.push(`no ${reqs[i3]} for function ${func.name} at ${connector.path}/main.sol:${func.firstLine}`)
}
}
}
return errors
} catch (error) {
return Promise.reject(error)
}
}
const checkName = async (connector) => {
try {
const strs = connector.code.split('\n')
let haveName = false
for (let index = strs.length - 1; index > 0; index--) {
const str = strs[index]
if (str.includes('string') && str.includes('public') && str.includes('name = ')) {
haveName = true
}
}
return haveName ? [] : [`name variable missing in ${connector.path}/main.sol`]
} catch (error) {
return Promise.reject(error)
}
}
async function checkMain () {
const errors = []
try {
const warnings = []
const connectors = await getConnectorsList()
for (let index = 0; index < connectors.length; index++) {
const { forbiddenErrors, code } = await checkForbidden(connectors[index].path)
connectors[index].code = code
connectors[index] = await parseCode(connectors[index])
const { eventsErrors, eventsWarnings } = await checkEvents(connectors[index])
const commentsErrors = await checkComments(connectors[index])
const nameErrors = await checkName(connectors[index])
errors.push(...forbiddenErrors)
errors.push(...eventsErrors)
errors.push(...commentsErrors)
errors.push(...nameErrors)
warnings.push(...eventsWarnings)
}
if (errors.length) {
console.log('\x1b[31m%s\x1b[0m', `Total errors: ${errors.length}`)
console.log('\x1b[31m%s\x1b[0m', errors.join('\n'))
} else {
console.log('\x1b[32m%s\x1b[0m', 'No Errors Found')
}
if (warnings.length) {
console.log('\x1b[33m%s\x1b[0m', `Total warnings: ${warnings.length}`)
console.log('\x1b[33m%s\x1b[0m', warnings.join('\n'))
} else {
console.log('\x1b[32m%s\x1b[0m', 'No Warnings Found')
}
} catch (error) {
console.error('check execution error:', error)
}
if (errors.length) throw new Error(errors.join('\n'))
}
module.exports = checkMain;