From 02382c17599604099d390179a485e77d8fcb140c Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Wed, 10 Oct 2018 12:34:09 -0700 Subject: [PATCH] Parser and Unparser --- packages/sol-meta/bin/sol-meta.js | 2 + packages/sol-meta/src/astToMock.ts | 6 + packages/sol-meta/src/cli.ts | 40 ++++++ packages/sol-meta/src/compiler.ts | 32 +++++ packages/sol-meta/src/index.ts | 0 packages/sol-meta/src/parser.ts | 3 + packages/sol-meta/src/transform.ts | 13 ++ packages/sol-meta/src/unparser.ts | 213 +++++++++++++++++++++++++++++ packages/sol-meta/test/dump | 0 packages/sol-meta/test/index.ts | 48 +++++++ 10 files changed, 357 insertions(+) create mode 100755 packages/sol-meta/bin/sol-meta.js create mode 100644 packages/sol-meta/src/astToMock.ts create mode 100644 packages/sol-meta/src/cli.ts create mode 100644 packages/sol-meta/src/compiler.ts create mode 100644 packages/sol-meta/src/index.ts create mode 100644 packages/sol-meta/src/parser.ts create mode 100644 packages/sol-meta/src/transform.ts create mode 100644 packages/sol-meta/src/unparser.ts create mode 100644 packages/sol-meta/test/dump create mode 100644 packages/sol-meta/test/index.ts diff --git a/packages/sol-meta/bin/sol-meta.js b/packages/sol-meta/bin/sol-meta.js new file mode 100755 index 0000000000..0e5b69af0e --- /dev/null +++ b/packages/sol-meta/bin/sol-meta.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/src/cli.js') diff --git a/packages/sol-meta/src/astToMock.ts b/packages/sol-meta/src/astToMock.ts new file mode 100644 index 0000000000..5d3d23ddad --- /dev/null +++ b/packages/sol-meta/src/astToMock.ts @@ -0,0 +1,6 @@ + +import { ASTNode } from 'solidity-parser-antlr'; + +export function astToMock(ast: ASTNode): ASTNode { + return ast; +} diff --git a/packages/sol-meta/src/cli.ts b/packages/sol-meta/src/cli.ts new file mode 100644 index 0000000000..0e670a6069 --- /dev/null +++ b/packages/sol-meta/src/cli.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// We need the above pragma since this script will be run as a command-line tool. + +import { logUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; +import 'source-map-support/register'; +import * as yargs from 'yargs'; + +import { Compiler } from './compiler'; + +const DEFAULT_CONTRACTS_LIST = '*'; +const SEPARATOR = ','; + +(async () => { + const argv = yargs + .option('contracts-dir', { + type: 'string', + description: 'path of contracts directory to compile', + }) + .option('contracts', { + type: 'string', + description: 'comma separated list of contracts to compile', + }) + .help().argv; + const contracts = _.isUndefined(argv.contracts) + ? undefined + : argv.contracts === DEFAULT_CONTRACTS_LIST + ? DEFAULT_CONTRACTS_LIST + : argv.contracts.split(SEPARATOR); + const opts = { + contractsDir: argv.contractsDir, + artifactsDir: argv.artifactsDir, + contracts, + }; + const compiler = new Compiler(opts); + await compiler.compileAsync(); +})().catch(err => { + logUtils.log(err); + process.exit(1); +}); diff --git a/packages/sol-meta/src/compiler.ts b/packages/sol-meta/src/compiler.ts new file mode 100644 index 0000000000..defe6d8f69 --- /dev/null +++ b/packages/sol-meta/src/compiler.ts @@ -0,0 +1,32 @@ +import fs = require('fs'); +import { parse } from './parser'; +import { unparse } from './unparser'; + +export interface CompilerOptions { + contractsDir: string; + contracts: string; +} + +export class Compiler { + + private readonly opts: CompilerOptions; + + constructor(opts?: CompilerOptions) { + this.opts = opts || { + contractsDir: '', + contracts: '', + } ; + } + + public async compileAsync() { + console.log(this.opts.contracts[0]); + const source = fs.readFileSync(this.opts.contracts[0], 'utf8'); + try { + const ast = parse(source); + console.log(unparse(ast)); + } catch (e) { + console.log(e); + } + console.log('Compiling'); + } +} diff --git a/packages/sol-meta/src/index.ts b/packages/sol-meta/src/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sol-meta/src/parser.ts b/packages/sol-meta/src/parser.ts new file mode 100644 index 0000000000..312a5c0616 --- /dev/null +++ b/packages/sol-meta/src/parser.ts @@ -0,0 +1,3 @@ +import * as parser from 'solidity-parser-antlr'; + +export const parse = (source) => parser.parse(source, {}); diff --git a/packages/sol-meta/src/transform.ts b/packages/sol-meta/src/transform.ts new file mode 100644 index 0000000000..82d722376e --- /dev/null +++ b/packages/sol-meta/src/transform.ts @@ -0,0 +1,13 @@ +import { ASTNode } from 'solidity-parser-antlr'; + +const visitor = { + + // If a function is not public, add a wrapper to make it public + FunctionDefinition: func => + func.visbility, + +} + +export function transform(ast: ASTNode): ASTNode { + return (visitor[ast.type] || (a => a))(ast); +} diff --git a/packages/sol-meta/src/unparser.ts b/packages/sol-meta/src/unparser.ts new file mode 100644 index 0000000000..9631e0f7f3 --- /dev/null +++ b/packages/sol-meta/src/unparser.ts @@ -0,0 +1,213 @@ +import { ASTNode } from 'solidity-parser-antlr'; + +const stresc = (s: string) => `\"${s}\"`; +const indent = (s: string) => `\t${s.replace(/\n/g, '\n\t')}`; +const block = (s: string) => `{\n${indent(s)}\n}`; +const unparen = (s: string) => s.replace(/^\((.*)\)$/, '$1'); + +const visitor = { + // Source level + + SourceUnit: ({children}) => + children.map(unparse).join('\n'), + + PragmaDirective: ({ name, value }) => + `pragma ${name} ${value};`, + + ImportDirective: ({ path, symbolAliases }) => + `import `+ + (symbolAliases ? `{${symbolAliases.map(([from, to]) => + from + (to ? ` as ${to}` : '') + ).join(', ')}} from `: '') + + `${stresc(path)};`, + + ContractDefinition: ({name, kind, baseContracts, subNodes}) => + `${kind} ${name} ${(baseContracts.length > 0 ? 'is ' : '')}` + + baseContracts.map(unparse).join(', ') + + block('\n' + subNodes.map(unparse).join('\n\n')), + + InheritanceSpecifier: ({baseName: {namePath}}) => + namePath, + + // Contract level + + UsingForDeclaration: ({typeName, libraryName}) => + `using ${libraryName} for ${unparse(typeName)};`, + + StateVariableDeclaration: ({variables}) => + variables.map(unparse).join(', ') + ';', + + StructDefinition: ({name, members}) => + `struct ${name} ${block(members.map(unparse).join(';\n') + ';')}`, + + EnumDefinition: ({name, members}) => + `enum ${name} ${block(members.map(unparse).join(',\n'))}`, + + EnumValue: ({name}) => + name, + + EventDefinition: ({ name, parameters }) => + `event ${name}${unparse(parameters)};`, + + ModifierDefinition: ({name, parameters, body}) => + `modifier ${name}${Array.isArray(parameters) ? '' : unparse(parameters)} ${unparse(body)}`, + // Note: when there is no parameter block, instead of an ASTNode there is a [] + + FunctionDefinition: ({visibility, name, parameters, body, modifiers, isConstructor, stateMutability}) => + (isConstructor ? 'constructor' : `function ${name}`) + + unparse(parameters) + ' ' + + (visibility && visibility != 'default' ? visibility + ' ' : '') + (stateMutability || '') + '\n' + + indent(modifiers.map(unparse).join('\n')) + '\n' + + (body ? unparse(body) : ';'), + + ParameterList: ({parameters}) => + `(${parameters.map(unparse).join(', ')})`, + + Parameter: ({typeName, name, storageLocation}) => + `${unparse(typeName)} ${storageLocation || ''} ${name || ''}`, + + ModifierInvocation: ({name, arguments: args}) => + `${name}(${args.map(unparse).join(', ')})`, + + // Statements + + Block: ({statements}) => + block(statements.map(unparse).join('\n')), + + VariableDeclarationStatement: ({variables, initialValue}) => + variables.map(unparse) + + (initialValue ? ` = ${unparse(initialValue)};` : ';'), + + ExpressionStatement: ({expression}) => + `${unparse(expression)};`, + + EmitStatement: ({eventCall}) => + `emit ${unparen(unparse(eventCall))};`, + + ReturnStatement: ({expression}) => + `return ${expression ? unparse(expression) : ''};`, + + BreakStatement: ({}) => + `break;`, + + ContinueStatement: ({}) => + `continue;`, + + ThrowStatement: ({}) => + `throw;`, + + IfStatement: ({condition, trueBody, falseBody}) => + `if (${unparse(condition)})\n${unparse(trueBody)}` + + (falseBody ? `else\n${unparse(falseBody)}` : ''), + + ForStatement: ({initExpression: i, conditionExpression: c, loopExpression: l, body}) => + `for (${unparse(i).replace(';','')}; ${unparse(c)}; ${unparse(l).replace(';','')}) ${unparse(body)}`, + + InlineAssemblyStatement: ({language, body}) => // TODO language + `assembly ${unparse(body)}`, + + // Types + + ElementaryTypeName: ({name}) => + name, + + UserDefinedTypeName: ({namePath}) => + namePath, + + ArrayTypeName: ({baseTypeName, length}) => + `${unparse(baseTypeName)}[${length ? unparse(length) : ''}]`, + + Mapping: ({keyType, valueType}) => + `mapping (${unparse(keyType)} => ${unparse(valueType)})`, + + // Expressions + + Identifier: ({ name }) => + name, + + BooleanLiteral: ({ value }) => + value ? 'true' : 'false', + + NumberLiteral: ({number, subdenomination}) => // TODO subdenomination + number, + + StringLiteral: ({value}) => + stresc(value), + + FunctionCall: ({expression, arguments: args, names}) => // TODO: names + `(${unparse(expression)}(${args.map(unparse).join(', ')}))`, + + Conditional: ({condition, trueExpression, falseExpression}) => + `(${unparse(condition)} ? ${unparse(trueExpression)} : ${unparse(falseExpression)})`, + + UnaryOperation: ({operator, subExpression, isPrefix}) => + `(${isPrefix ? operator : ''}${unparse(subExpression)}${isPrefix ? '' : operator})`, + + BinaryOperation: ({operator, left, right}) => + `(${unparse(left)} ${operator} ${unparse(right)})`, + + MemberAccess: ({expression, memberName}) => + `(${unparse(expression)}.${memberName})`, + + IndexAccess: ({base, index}) => + `(${unparse(base)}[${unparse(index)}])`, + + ElementaryTypeNameExpression: ({typeName}) => + `(${unparse(typeName)})`, + + VariableDeclaration: ({typeName, name, visibility, isDeclaredConst, isIndexed, expression}) => + `${unparse(typeName)} ` + + (isIndexed ? 'indexed ' : '') + + (visibility && visibility != 'default' ? visibility + ' ' : '') + + (isDeclaredConst ? 'constant ' : '') + + `${name}` + + (expression ? ` = ${unparse(expression)}` : ''), + + NewExpression: ({typeName}) => + `(new ${unparse(typeName)})`, + + TupleExpression: ({components}) => + `[${components.map(unparse).join(', ')}]`, + + // Assembly + + AssemblyBlock: ({operations}) => + block(operations.map(unparse).join('\n')), + + AssemblyAssignment: ({names, expression}) => + `${names.map(unparse).join(', ')} := ${unparse(expression)}`, + + AssemblyLocalDefinition: ({names, expression}) => + `let ${names.map(unparse).join(', ')} := ${unparse(expression)}`, + + AssemblyCall: ({functionName, arguments: args}) => + args.length == 0 ? + functionName : + `${functionName}(${args.map(unparse).join(', ')})`, + + AssemblyIf: ({condition, body}) => + `if ${unparse(condition)} ${unparse(body)}`, + + AssemblyFor: ({pre, condition, post, body}) => + `for ${[pre, condition, post, body].map(unparse).join(' ')}`, + + AssemblySwitch: ({expression, cases}) => + `switch ${unparse(expression)}\n${cases.map(unparse).join('\n')}`, + + AssemblyCase: ({value, block}) => + `case ${unparse(value)} ${unparse(block)}` + + DecimalNumber: ({ value }) => + value, + + HexNumber: ({ value }) => + value, +} + +export function unparse(ast: ASTNode): string { + return (visitor[ast.type] || (a => { + console.log(a); + console.trace(); + return `<${a.type}>`; + }))(ast); +} diff --git a/packages/sol-meta/test/dump b/packages/sol-meta/test/dump new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sol-meta/test/index.ts b/packages/sol-meta/test/index.ts new file mode 100644 index 0000000000..88181e05eb --- /dev/null +++ b/packages/sol-meta/test/index.ts @@ -0,0 +1,48 @@ +import * as chai from 'chai'; +import 'mocha'; +import * as glob from 'glob'; +import * as fs from 'fs'; +import * as path from 'path'; +import { parse } from '../src/parser'; +import { unparse } from '../src/unparser'; + +const expect = chai.expect; + +const promisify = (func) => (...args) => + new Promise((resolve, reject) => + func(...args, (error, result) => + error ? reject(error) : resolve(result))); + +const findContracts = searchPath => + glob.sync(searchPath).map(file => ({ + name: path.basename(file, '.sol') + ` (${file})`, + source: fs.readFileSync(file, 'utf8') + })); + +const contracts = findContracts('../contracts/src/**/*.sol'); + +describe('Parser', () => { + + it('should have test contracts', () => { + expect(contracts).to.have.lengthOf.above(10); + }); + + contracts.forEach(({name, source}) => + it(`should parse ${name}`, () => { + parse(source); + }) + ); + +}); + +describe.only('Unparser', () => { + + contracts.forEach(({name, source}) => + it(`should unparse ${name}`, () => { + const ast = parse(source); + const src = unparse(ast) ; + const ast2 = parse(src); + //expect(ast2).to.deep.equal(ast); + }) + ); +})