Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Exports and add option to leave comments (false by default) #25

Merged
merged 8 commits into from
Jan 5, 2020
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,6 @@ sol-merger --help

More info about `glob` available at [node-glob repository](https://github.com/isaacs/node-glob)

# Known Issues

Currently if you use named imports from the file that have multiple exports
(contacts, libraries, interfaces) it only imports the ones that are requested
without analyzing the dependencies inside file. That means you should NOT use
named imports for multiple exports OR import all the required exports.

See tests for more examples.

# Plugin for VSCode
Expand Down
8 changes: 6 additions & 2 deletions bin/sol-merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import Debug from 'debug';
import { Merger } from '../lib/merger';
import { done } from '../utils/done';
import program from 'commander';
import { version } from '../package.json';

const debug = Debug('sol-merger:debug');

let inputGlob: string, outputDir: string, append: string;

program
.version(`v${version}`, '-v, --version')
.option('-a, --append [append]', '', /^([a-zA-Z_]+)$/)
.option('-c, --with-comments', `Doesn't remove comment from exports`, false)
.arguments('<glob> [outputDir]')
.action((_glob, _outputDir) => {
inputGlob = _glob;
Expand All @@ -36,7 +39,8 @@ if (outputDir) {
: path.join(process.cwd(), outputDir);
}

debug(outputDir);
debug('Output directory', outputDir);
debug('With comments?', program.withComments);

glob(
inputGlob,
Expand All @@ -60,7 +64,7 @@ async function execute(err: Error, files: string[]) {
}

const promises = files.map(async (file) => {
const merger = new Merger({ delimeter: '\n\n' });
const merger = new Merger({ delimeter: '\n\n', removeComments: !program.withComments });
let result: string;
result = await merger.processFile(file, true);
let outputFile: string;
Expand Down
100 changes: 31 additions & 69 deletions lib/fileAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import Debug from 'debug';
import fs from 'fs-extra';
import parser from 'solidity-parser-antlr';
import stripComments from 'strip-json-comments';
import { RegistredImport } from './merger';

const error = Debug('sol-merger:error');

export class FileAnalyzer {
filename: string;
removeComments: boolean;
/**
* Builds the function body depending on the export
*/
Expand Down Expand Up @@ -36,8 +41,9 @@ export class FileAnalyzer {
/**
* Filename to read to get contract data
*/
constructor(filename: string) {
constructor(filename: string, removeComments: boolean = true) {
this.filename = filename;
this.removeComments = removeComments;
}

/**
Expand All @@ -46,7 +52,9 @@ export class FileAnalyzer {
async analyze(): Promise<FileAnalyzerResult> {
await fs.stat(this.filename);
let contents = await fs.readFile(this.filename, { encoding: 'utf-8' });
contents = stripComments(contents, { whitespace: false });
if (this.removeComments) {
contents = stripComments(contents, { whitespace: false });
}
const imports = this.analyzeImports(contents);
const exports = this.analyzeExports(contents);
return {
Expand Down Expand Up @@ -129,76 +137,30 @@ export class FileAnalyzer {
*
*/
analyzeExports(contents: string): FileAnalyzerExportsResult[] {
const exportRegex = /(contract|library|interface)\s+([a-zA-Z_$][a-zA-Z_$0-9]*)\s*([\s\S]*?)\{/g;
const isRegex = /^is\s*[a-zA-Z_$][a-zA-Z_$0-9]*(.[a-zA-Z_$][a-zA-Z_$0-9]*)?(\([\s\S]*?\))?(,\s*?[a-zA-Z_$][a-zA-Z_$0-9]*(.[a-zA-Z_$][a-zA-Z_$0-9]*)?(\([\s\S]*?\))?)*\s*$/;
const results = [];
let group: RegExpExecArray;
while ((group = exportRegex.exec(contents))) {
const [, type, name, is] = group;
// Checking that `is` clause is correct
if (is.trim() && !isRegex.test(is.trim())) {
continue;
}
const body = this.findBodyEnd(
contents,
group.index + group[0].length - 1,
);
results.push({
type,
name,
is,
body,
try {
const ast = parser.parse(contents, { loc: true, range: true });
const results: FileAnalyzerExportsResult[] = [];
const exportRegex = /(contract|library|interface)\s+([a-zA-Z_$][a-zA-Z_$0-9]*)\s*([\s\S]*?)\{/;
parser.visit(ast, {
ContractDefinition: (node) => {
const contract = contents.substring(node.range[0], node.range[1] + 1);
const group = exportRegex.exec(contract);
const [match, _, __, is] = group;
results.push({
is: is,
name: node.name,
type: node.kind as any,
body: contract.substring(match.length - 1),
});
},
});
}
return results;
}

/**
* @param contents file contents
* @param start start of the body, start must be pointing to "{"
* @returns body of the export
*/
findBodyEnd(contents: string, start: number): string {
let deep = 1;
let idx = start + 1;
let inString = false;
let isSingleQuotedString = false;
while (deep !== 0 && idx < contents.length) {
if (contents[idx] === '}' && !inString) {
deep -= 1;
return results;
} catch (e) {
if (e instanceof (parser as any).ParserError) {
error(e.errors);
}
if (contents[idx] === '{' && !inString) {
deep += 1;
}

if (contents[idx] === '"') {
if (
(inString && contents[idx - 1] !== '\\' && !isSingleQuotedString) ||
!inString
) {
isSingleQuotedString = false;
inString = !inString;
}
}

if (contents[idx] === "'") {
if (
(inString && contents[idx - 1] !== '\\' && isSingleQuotedString) ||
!inString
) {
isSingleQuotedString = true;
inString = !inString;
}
}

idx += 1;
}
if (deep !== 0) {
throw new Error(
'Export is not correct. Has more opening brackets then closing.',
);
return [];
}
return contents.substring(start, idx);
}
}

Expand Down
13 changes: 5 additions & 8 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { Merger } from './merger';
import { FileAnalyzer } from './fileAnalyzer';
import { Merger, SolMergerOptions } from './merger';

const merge = async (file: string) => {
const merger = new Merger();
const merge = async (file: string, options: SolMergerOptions = {}) => {
const merger = new Merger(options);
return merger.processFile(file, true);
}

export {
Merger,
FileAnalyzer,
merge
}
export { Merger, FileAnalyzer, merge };

26 changes: 16 additions & 10 deletions lib/merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ const error = Debug('sol-merger:error');
const log = Debug('sol-merge:log');

export class Merger {
delimeter: string;
registeredImports: RegistredImport[];
nodeModulesRoot: string;
delimeter: string = this.options.delimeter || '\n\n';
removeComments: boolean;

constructor(options: SolMergerOptions = {}) {
const { delimeter } = options;
registeredImports: RegistredImport[] = [];
nodeModulesRoot: string = null;

this.delimeter = delimeter || '\n\n';
this.registeredImports = [];
this.nodeModulesRoot = null;
constructor(private options: SolMergerOptions = {}) {
if ('removeComments' in options) {
this.removeComments = options.removeComments;
} else {
this.removeComments = true;
}
}

getPragmaRegex() {
Expand All @@ -38,7 +40,7 @@ export class Merger {
let result = '';
const pragmaRegex = this.getPragmaRegex();
let group = pragmaRegex.exec(contents);
while(group) {
while (group) {
result += group[1] + '\n';
group = pragmaRegex.exec(contents);
}
Expand Down Expand Up @@ -66,7 +68,10 @@ export class Merger {
this.registeredImports = [];
this.nodeModulesRoot = await this.getNodeModulesPath(file);
}
const analyzedFile = await new FileAnalyzer(file).analyze();
const analyzedFile = await new FileAnalyzer(
file,
this.removeComments,
).analyze();

let result = '';

Expand Down Expand Up @@ -207,6 +212,7 @@ export class Merger {

export interface SolMergerOptions {
delimeter?: string;
removeComments?: boolean;
}

export interface RegistredImport {
Expand Down
19 changes: 16 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
"license": "BSD-2-Clause",
"dependencies": {
"cli-color": "^1.4.0",
"commander": "^2.19.0",
"commander": "^4.0.1",
"debug": "^4.1.1",
"fs-extra": "^8.0.1",
"glob": "^7.1.2",
"solidity-parser-antlr": "^0.4.11",
"strip-json-comments": "^3.0.1"
},
"pre-commit": [
Expand Down
35 changes: 35 additions & 0 deletions test/compiled/ContactWithKeywordsInsideString.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
pragma solidity ^0.5.0;


contract A {
uint private a;

function setA(uint _a) public {
a = _a;
}

function getA() public view returns (uint) {
return a;
}
}

contract ConflictingInheritance {
function thisContractWillNotBeCorrectlyMerged() public pure {
require(false, "Because it will have they keyword contract in this string");
}

}

contract B {
uint private b;

function setB(uint _b) public {
b = _b;
}

function getB() public view returns (uint) {
return b;
}
}

contract Example is A, B, ConflictingInheritance {}
33 changes: 33 additions & 0 deletions test/compiled/LocalImportsWithComments.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pragma solidity ^0.4.11;
pragma experimental ABIEncoderV2;


contract Ownable {
address public owner;

function Ownable() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

function transferOwnership(address newOwner) onlyOwner {
if (newOwner != address(0)) {
owner = newOwner;
}
}

}

contract MyOwned is Ownable {
// Super important comment here
string public constant name = "My Owned";

/**
* Super important description here
*/
function MyOwned() {}
}
Loading