-
-
Notifications
You must be signed in to change notification settings - Fork 129
/
cli.js
415 lines (358 loc) · 14.6 KB
/
cli.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
/**
* @fileoverview Main CLI object which makes use of the Linter's API to access functionality
* @author Raghav Dua <[email protected]>
*/
"use strict";
let cli = require("commander"),
fs = require("fs"),
fsUtils = require("./utils/fs-utils"),
path = require("path"),
{ EOL } = require("os"),
chokidar = require("chokidar"),
traverse = require("sol-digger"),
solium = require("./solium"),
sum = require("lodash/sum"),
version = require("../package.json").version;
let CWD = process.cwd(),
SOLIUMRC_FILENAME = ".soliumrc.json",
SOLIUMRC_FILENAME_ABSOLUTE = path.join(CWD, SOLIUMRC_FILENAME),
SOLIUMIGNORE_FILENAME = ".soliumignore",
SOLIUMIGNORE_FILENAME_ABSOLUTE = path.join(CWD, SOLIUMIGNORE_FILENAME),
DEFAULT_SOLIUMIGNORE_PATH = `${__dirname}/cli-utils/.default-solium-ignore`,
DEFAULT_SOLIUMRC_PATH = `${__dirname}/cli-utils/.default-soliumrc.json`;
let errorCodes = { ERRORS_FOUND: 1, NO_SOLIUMRC: 3, WRITE_FAILED: 4, INVALID_PARAMS: 5, FILE_NOT_FOUND: 6 };
/**
* Create default configuration files in the user's directory
* @returns {void}
*/
function setupDefaultUserConfig() {
createDefaultConfigJSON();
createDefaultSoliumIgnore();
}
/**
* Synchronously write the passed configuration to the file whose absolute path is SOLIUMRC_FILENAME_ABSOLUTE
* @param {Object} config User Configuration object
* @returns {void}
*/
function writeConfigFile(config) {
try {
fs.writeFileSync(
SOLIUMRC_FILENAME_ABSOLUTE,
JSON.stringify(config, null, 2)
);
} catch (e) {
errorReporter.reportFatal(
`An error occurred while writing to ${SOLIUMRC_FILENAME_ABSOLUTE}:${EOL}${e.message}`);
process.exit(errorCodes.WRITE_FAILED);
}
}
/**
* Copy data from cli-utils/.default-solium-ignore to (newly created) .soliumignore in user's root directory
* @returns {void}
*/
function createDefaultSoliumIgnore() {
try {
fs.writeFileSync(
SOLIUMIGNORE_FILENAME_ABSOLUTE,
fs.readFileSync(DEFAULT_SOLIUMIGNORE_PATH)
);
} catch (e) {
errorReporter.reportFatal(
`An error occurred while writing to ${SOLIUMIGNORE_FILENAME_ABSOLUTE}:${EOL}${e.message}`);
process.exit(errorCodes.WRITE_FAILED);
}
}
/**
* Create default solium configuration JSON in user's current working directory.
* This file enables all the built-in lint rules
*/
function createDefaultConfigJSON() {
writeConfigFile(require(DEFAULT_SOLIUMRC_PATH));
}
/**
* Lint a source code string based on user settings. If autofix is enabled, write the fixed code back to file.
* @param {String} sourceCode The source code to be linted
* @param {Object} userConfig User configuration
* @param {Object} errorReporter The error reporter to use
* @param {String} fileName (optional) File name to use when reporting errors
* @returns {Integer} numOfErrors Number of Lint ERRORS that occured.
*/
function lintString(sourceCode, userConfig, errorReporter, fileName) {
let lintErrors, fixesApplied;
try {
if (userConfig.options.autofix || userConfig.options.autofixDryrun) {
let result = solium.lintAndFix(sourceCode, userConfig);
lintErrors = result.errorMessages;
if (userConfig.options.autofix) {
applyFixes(fileName, result);
fixesApplied = result.fixesApplied;
} else {
errorReporter.reportDiff(fileName,
sourceCode, result.fixedSourceCode, result.fixesApplied.length);
}
} else {
lintErrors = solium.lint(sourceCode, userConfig);
}
} catch (e) {
// Don't abort in case of a parse error, just report it as a normal lint issue.
if (e.name !== "SyntaxError") {
const messageOrStackrace = userConfig.options.debug ? e.stack : e.message;
errorReporter.reportFatal(`An error occured while linting over ${fileName}:${EOL}${messageOrStackrace}`);
process.exit(errorCodes.ERRORS_FOUND);
}
lintErrors = [{
ruleName: "",
type: "error",
message: `Syntax error: unexpected token ${e.found}`,
line: e.location.start.line,
column: e.location.start.column
}];
}
// If any lint/internal errors/warnings exist, report them
lintErrors.length &&
errorReporter.report(fileName, sourceCode, lintErrors, fixesApplied);
return lintErrors.reduce(function(numOfErrors, err) {
return err.type === "error" ? numOfErrors+1 : numOfErrors;
}, 0);
}
function applyFixes(fileName, lintResult) {
lintResult.fixesApplied.length && fs.writeFileSync(fileName, lintResult.fixedSourceCode);
}
/**
* Lint a file based on user settings
* @param {String} fileName The path to the file to be linted
* @param {Object} userConfig User configuration
* @param {Object} errorReporter The error reporter to use
* @returns {Integer} numOfErrors Number of Lint ERRORS that occured (the result returned by lintString())
*/
function lintFile(fileName, userConfig, errorReporter) {
let sourceCode;
try {
sourceCode = fs.readFileSync(fileName, "utf8");
} catch (e) {
errorReporter.reportFatal("Unable to read " + fileName + ": " + e.message);
process.exit(errorCodes.FILE_NOT_FOUND);
}
return lintString(sourceCode, userConfig, errorReporter, fileName);
}
/**
* Function that calls Solium object's linter based on user settings.
* If not given, we lint the entire directory's (and sub-directories') solidity files.
* @param {Object} userConfig User's configurations that contain information about which linting rules to apply
* @param {String} filename (optional) The single file to be linted.
* @returns {Integer} totalNumOfErrors Total no. of errors found throughout the codebase (directory) linted.
*/
function lint(userConfig, input, ignore, errorReporter) {
let filesToLint, errorCount;
//If filename is provided, lint it. Otherwise, lint over current directory & sub-directories
if (input.file) {
if (!fsUtils.isFile(input.file)) {
errorReporter.reportFatal(`${input.file} is not a valid file`);
process.exit(errorCodes.INVALID_PARAMS);
}
filesToLint = [input.file];
} else if (input.dir) {
if (!fsUtils.isDirectory(input.dir)) {
errorReporter.reportFatal(`${input.dir} is not a valid directory`);
process.exit(errorCodes.INVALID_PARAMS);
}
filesToLint = traverse(input.dir, ignore);
}
if (filesToLint) {
errorCount = sum(filesToLint.map(function(file, index) {
userConfig.options.returnInternalIssues = (index === 0);
return lintFile(file, userConfig, errorReporter);
}));
} else if (input.stdin) {
// This only works on *nix. Need to fix to enable stdin input in windows.
let sourceCode = fs.readFileSync("/dev/stdin", "utf-8");
userConfig.options.returnInternalIssues = true;
errorCount = lintString(sourceCode, userConfig, errorReporter, "[stdin]");
} else {
errorReporter.reportFatal("Must specify input for linter using --file, --dir or --stdin");
process.exit(errorCodes.INVALID_PARAMS);
}
errorReporter.finalize && errorReporter.finalize();
return errorCount;
}
/**
* Function responsible for defining all the available commandline options & version information
* @param {Object} cliObject Commander Object handling the cli
*/
function createCliOptions(cliObject) {
function collect(val, memo) {
memo.push(val);
return memo;
}
cliObject
.version(`Solium version ${version}`)
.description("Linter to find & fix style and security issues in Solidity smart contracts.")
.usage("[options] <keyword>")
.option("-i, --init", "Create default rule configuration files")
.option("-f, --file [filepath::String]", "Solidity file to lint")
.option("-d, --dir [dirpath::String]", "Directory containing Solidity files to lint")
.option("-R, --reporter [name::String]", "Format to report lint issues in (pretty | gcc)", "pretty")
.option("-c, --config [filepath::String]", "Path to the .soliumrc configuration file")
.option("-, --stdin", "Read input file from stdin")
.option("--fix", "Fix Lint issues where possible")
.option("--fix-dry-run", "Output fix diff without applying it")
.option("--debug", "Display debug information")
.option("--watch", "Watch for file changes")
.option("--hot", "(Deprecated) Same as --watch")
.option("--no-soliumignore", "Do not look for .soliumignore file")
.option("--no-soliumrc", "Do not look for soliumrc configuration file")
.option(
"--rule [rule]",
"Rule to execute. This overrides the specified rule's configuration in soliumrc if present",
collect,
[]
)
.option(
"--plugin [plugin]",
"Plugin to execute. This overrides the specified plugin's configuration in soliumrc if present",
collect,
[]
);
}
/**
* Takes a name and returns an error reporter
* @param {String} name Name of the reporter
* @returns {Object} reporter The reporter whose name was supplied.
*/
function getErrorReporter(name) {
try {
return require("./reporters/" + name);
} catch (e) {
throw new Error(
`Invalid reporter "${name}". Valid reporters are "gcc" and "pretty"`
);
}
}
/**
* Entry point to the CLI reponsible for initiating linting process based on command-line arguments
* @param {Array} programArgs Commandline arguments
*/
function execute(programArgs) {
let userConfig = {}, ignore, errorReporter;
createCliOptions(cli);
programArgs.length === 2 ? cli.help() : cli.parse(programArgs);
if (cli.init) {
return setupDefaultUserConfig();
}
try {
errorReporter = getErrorReporter(cli.reporter);
} catch (e) {
process.stderr.write(`[Fatal error] ${e.message}${EOL}`);
process.exit(errorCodes.INVALID_PARAMS);
}
if (cli.soliumrc) {
/**
* If cli.config option is NOT specified, then resort to .soliumrc in current dir.
* Else,
* If path is absolute, assign as-it-is.
* Else (relative pathing) join path with current dir.
*/
const soliumrcAbsPath = cli.config ?
(path.isAbsolute(cli.config) ? cli.config : path.join(CWD, cli.config)) :
SOLIUMRC_FILENAME_ABSOLUTE;
try {
userConfig = require(soliumrcAbsPath);
} catch (e) {
// Check if soliumrc file exists. If yes, then the file is in an invalid format.
if (fs.existsSync(soliumrcAbsPath)) {
errorReporter.reportFatal(`An invalid ${SOLIUMRC_FILENAME} was provided. ${e.message}`);
} else {
if (cli.config) {
errorReporter.reportFatal(`${soliumrcAbsPath} does not exist.`);
} else {
errorReporter.reportFatal(`Couldn't find ${SOLIUMRC_FILENAME} in the current directory.`);
}
}
process.exit(errorCodes.NO_SOLIUMRC);
}
}
//if custom rules' file is set, make sure we have its absolute path
if (
userConfig ["custom-rules-filename"] &&
!path.isAbsolute(userConfig ["custom-rules-filename"])
) {
userConfig ["custom-rules-filename"] = path.join(
CWD, userConfig ["custom-rules-filename"]
);
}
// Pass cli arguments that modify the behaviour of upstream functions.
userConfig.options = {
autofix: Boolean(cli.fix),
autofixDryrun: Boolean(cli.fixDryRun),
debug: Boolean(cli.debug)
};
if (userConfig.options.autofixDryrun) {
if (userConfig.options.autofix) {
return errorReporter.reportFatal("Cannot use both --fix and --fix-dry-run");
}
if (cli.reporter != "pretty") {
return errorReporter.reportFatal("Option --fix-dry-run is only supported with pretty reporter");
}
}
userConfig.plugins = userConfig.plugins || [];
userConfig.rules = userConfig.rules || {};
for (const plugin of cli.plugin) {
userConfig.plugins.push(plugin);
}
for (const rule of cli.rule) {
// If no ":" was found, it means only the rule's name was specified.
// Treat it as an error and adopt its default configuration options.
if (!rule.includes(":")) {
userConfig.rules[rule] = "error";
continue;
}
let [key, value] = rule.split(":").map(i => i.trim());
try {
value = JSON.parse(value);
} catch (e) {
errorReporter.reportFatal(`There was an error trying to parse '${rule}': ${e.message}`);
process.exit(errorCodes.INVALID_PARAMS);
}
userConfig.rules[key] = value;
}
//get all files & folders to ignore from .soliumignore
if (cli.soliumignore) {
try {
ignore = fs.readFileSync(SOLIUMIGNORE_FILENAME_ABSOLUTE, "utf8").split(EOL);
} catch (e) {
if (e.code === "ENOENT") {
errorReporter.reportInternal(
"No '.soliumignore' found. Use --no-soliumignore to make this warning go away.");
} else {
errorReporter.reportInternal(`Failed to read '.soliumignore': ${e.message}`);
}
}
}
if (cli.hot) {
// --hot is equivalent to --watch in functionality, is a legacy option
cli.watch = true;
}
if (cli.watch) {
if (cli.stdin) {
return errorReporter.reportFatal("Cannot watch files when reading from stdin");
}
if (cli.fix) {
return errorReporter.reportFatal("Automatic code formatting is not supported in watch mode.");
}
}
let errorCount = lint(userConfig, { file: cli.file, dir: cli.dir, stdin: cli.stdin }, ignore, errorReporter);
if (cli.watch) {
let spy = chokidar.watch(CWD);
spy.on("change", function() {
console.log("\x1Bc"); // clear the console
console.log(`File change detected. Start linting.${EOL}`);
lint(userConfig, { file: cli.file, dir: cli.dir }, ignore, errorReporter); //lint on subsequent changes (hot)
console.log(`Linting complete. Watching for file changes.${EOL}`);
});
} else if (errorCount > 0) {
process.exit(errorCodes.ERRORS_FOUND);
}
}
module.exports = {
execute: execute
};