Comparing version 0.5.5 to 1.0.0
@@ -32,2 +32,30 @@ { | ||
"conditionals-whitespace": { | ||
"enabled": true, | ||
"recommended": true, | ||
"type": "error", | ||
"description": "Ensure that there is exactly one space between conditional operators and parenthetic blocks" | ||
}, | ||
"comma-whitespace": { | ||
"enabled": true, | ||
"recommended": true, | ||
"type": "error", | ||
"description": "Ensure that there is no whitespace or comments between comma delimited elements and commas" | ||
}, | ||
"semicolon-whitespace": { | ||
"enabled": true, | ||
"recommended": true, | ||
"type": "error", | ||
"description": "Ensure that there is no whitespace or comments before semicolons" | ||
}, | ||
"function-whitespace": { | ||
"enabled": true, | ||
"recommended": true, | ||
"type": "error", | ||
"description": "Ensure function calls and declaration have (or don't have) whitespace in appropriate locations" | ||
}, | ||
"lbrace": { | ||
@@ -89,2 +117,9 @@ "enabled": true, | ||
"quotes": { | ||
"enabled": true, | ||
"recommended": true, | ||
"type": "error", | ||
"description": "Ensure that all strings use only 1 style - either double quotes or single quotes." | ||
}, | ||
"blank-lines": { | ||
@@ -104,2 +139,9 @@ "enabled": true, | ||
"arg-overflow": { | ||
"enabled": true, | ||
"recommended": false, | ||
"type": "warning", | ||
"description": "In the case of 4+ elements in the same line require they are instead put on a single line each" | ||
}, | ||
"whitespace": { | ||
@@ -106,0 +148,0 @@ "enabled": true, |
@@ -9,2 +9,7 @@ /** | ||
/** | ||
* Merge all fixer packets into 1 packet and return it. | ||
* @params {(Array|Object)} fixesArrayOrObject Fixer packet(s) to be merged into a single fixer packet. | ||
* @params {String} sourceCode Source code. | ||
*/ | ||
module.exports = function (fixesArrayOrObject, sourceCode) { | ||
@@ -18,6 +23,2 @@ // If argument is already a single fixes packet, return. | ||
if (!fixes.length) { | ||
throw new Error ('Array doesn\'t contain any Fixer Objects.'); | ||
} | ||
if (fixes.length === 1) { | ||
@@ -27,2 +28,3 @@ return fixesArrayOrObject [0]; | ||
// Sort fixer packets in top-down approach | ||
fixes.sort (function (a, b) { | ||
@@ -32,2 +34,4 @@ return (a.range [0] - b.range [0]) || (a.range [1] - b.range [1]); | ||
// Take start of first fix and end of last fix to merge them all into 1 big fixer packet, | ||
// combining all the code in between. | ||
var start = fixes [0].range [0], end = fixes [fixes.length - 1].range [1]; | ||
@@ -37,4 +41,11 @@ var text = '', cursor = Number.MIN_SAFE_INTEGER; | ||
fixes.forEach (function (fix) { | ||
if (cursor >= fix.range [0]) { | ||
throw new Error ('Fixer objects in a single Fixer Array cannot overlap.'); | ||
/** | ||
* Condition below is '>' instead of '>=' on purpose. | ||
* Say we have 2 packets- {range: [12, 20]} & {range: [20, 28], text: 'world'}. | ||
* These should NOT conflict. After 1st packet (which actually makes changes to only 12 to 19 index), | ||
* cursor is set to 20. At start of 2nd packet, the slice (20, 20) returns empty string and hence, | ||
* there is no damage. | ||
*/ | ||
if (cursor > fix.range [0]) { | ||
throw new Error ('2 or more fix objects in the Fixer Array must not overlap.'); | ||
} | ||
@@ -41,0 +52,0 @@ |
@@ -10,3 +10,46 @@ /** | ||
function validateArgs (args) { | ||
if (args.hasOwnProperty ('node')) { | ||
if (!astUtils.isASTNode (args.node)) { | ||
throw new Error ('A valid AST node was not provided.'); | ||
} | ||
} | ||
if (args.hasOwnProperty ('text')) { | ||
var text = args.text; | ||
// There are no restriction on string length | ||
if (typeof text !== 'string') { | ||
throw new Error ('A valid string was not provided.'); | ||
} | ||
} | ||
if (args.hasOwnProperty ('range')) { | ||
var range = args.range; | ||
if (!(Array.isArray (range) && range.length === 2 && Number.isInteger (range [0]) && | ||
Number.isInteger (range [1]) && range [0] >= 0 && range [1] >= 0)) { | ||
throw new Error ( | ||
'A valid range object was not provided. Should be an Array of 2 unsigned Integers.'); | ||
} | ||
} | ||
if (args.hasOwnProperty ('index')) { | ||
var index = args.index; | ||
if (!(Number.isInteger (index) && index >= 0)) { | ||
throw new Error ('A valid index was not provided. Must be an unsigned Integer.'); | ||
} | ||
} | ||
if (args.hasOwnProperty ('char')) { | ||
var char = args.char; | ||
if (!(typeof char === 'string' && char.length === 1)) { | ||
throw new Error ('A valid character was not provided. Must be a string with length of 1.'); | ||
} | ||
} | ||
} | ||
function insertTextAt (index, text) { | ||
@@ -19,17 +62,7 @@ return { | ||
function validateArgs (node, text) { | ||
if (!astUtils.isASTNode (node)) { | ||
throw new Error ('A valid AST node was not provided.'); | ||
} | ||
if (!(text && typeof text === 'string')) { | ||
throw new Error ('A valid string was not provided.'); | ||
} | ||
} | ||
var ruleFixerAPI = { | ||
insertTextAfter (node, text) { | ||
validateArgs (node, text); | ||
validateArgs ({node: node, text: text}); | ||
return this.insertTextAfterRange ([node.start, node.end], text); | ||
@@ -39,3 +72,3 @@ }, | ||
insertTextBefore (node, text) { | ||
validateArgs (node, text); | ||
validateArgs ({node: node, text: text}); | ||
return this.insertTextBeforeRange ([node.start, node.end], text); | ||
@@ -49,3 +82,3 @@ }, | ||
replaceText (node, text) { | ||
validateArgs (node, text); | ||
validateArgs ({node: node, text: text}); | ||
return this.replaceTextRange ([node.start, node.end], text); | ||
@@ -55,2 +88,3 @@ }, | ||
insertTextAfterRange (range, text) { | ||
validateArgs ({range: range, text: text}); | ||
return insertTextAt (range [1], text); | ||
@@ -60,2 +94,3 @@ }, | ||
insertTextBeforeRange (range, text) { | ||
validateArgs ({range: range, text: text}); | ||
return insertTextAt (range [0], text); | ||
@@ -65,6 +100,13 @@ }, | ||
removeRange (range) { | ||
validateArgs ({range: range}); | ||
return this.replaceTextRange (range, ''); | ||
}, | ||
replaceChar (index, newChar) { | ||
validateArgs ({index: index, char: newChar}); | ||
return this.replaceTextRange ([index, index + 1], newChar); | ||
}, | ||
replaceTextRange (range, text) { | ||
validateArgs ({range: range, text: text}); | ||
return { | ||
@@ -71,0 +113,0 @@ range: range, |
@@ -23,2 +23,8 @@ /** | ||
/** | ||
* Apply fixes to source code depending on whichever errors can be fixed. | ||
* @param {String} sourceCode Code to fix | ||
* @param {Array} errorMessages Error objects that describe the error and possibly how to fix it. | ||
* @returns {Object} fixed Contains fixed code and information about fixes applied & remaining un-fixed errors. | ||
*/ | ||
applyFixes: function (sourceCode, errorMessages) { | ||
@@ -32,3 +38,5 @@ var fixedSourceCode = '', fixes = [], fixesApplied = [], remainingMessages = []; | ||
// If this fix overlaps with the previous one or has negaive range, return. | ||
if (cursor >= start || start > end) { | ||
// Note that when cursor === start, its NOT an overlap since when source code in range | ||
// [i, j] is being edited, the code covered is actually from i to j-1. | ||
if (cursor > start || start > end) { | ||
return false; | ||
@@ -43,7 +51,13 @@ } | ||
// Segregate errors that can be fixed from those that can't for sure. | ||
errorMessages.forEach (function (msg) { | ||
if (msg.fix) { | ||
// If msg.fix is an Array of fix packets, merge them into a single fix packet. | ||
msg.fix = mergeFixes (msg.fix, sourceCode); | ||
try { | ||
msg.fix = mergeFixes (msg.fix, sourceCode); | ||
} catch (e) { | ||
throw new Error ('An error occured while applying fix of rule "' | ||
+ msg.ruleName + '" for error "' + msg.message + '": ' + e.message); | ||
} | ||
return fixes.push (msg); | ||
@@ -55,2 +69,5 @@ } | ||
// Fixes will be applied in top-down approach. The fix that arrives first (line-wise, followed by column-wise) | ||
// gets applied first. But if current fix is applied successfully & the next one overlaps the current one, | ||
// then the next one is simply skipped. Hence, it is NOT guranteed that all fixes will be applied. | ||
fixes.sort (compareMessagesByFixRange).forEach (function (msg) { | ||
@@ -57,0 +74,0 @@ if (attemptFix (msg.fix)) { |
151
lib/cli.js
@@ -23,10 +23,6 @@ /** | ||
SOLIUMIGNORE_FILENAME_ABSOLUTE = path.join (CWD, SOLIUMIGNORE_FILENAME), | ||
DEFAULT_SOLIUMIGNORE_PATH = __dirname + '/cli-utils/.default-solium-ignore'; | ||
DEFAULT_SOLIUMIGNORE_PATH = __dirname + '/cli-utils/.default-solium-ignore', | ||
DEFAULT_SOLIUMRC_PATH = __dirname + '/cli-utils/.default-soliumrc.json'; | ||
var errorCodes = { | ||
ERRORS_FOUND: 1, | ||
NO_SOLIUMRC: 3, | ||
WRITE_FAILED: 4, | ||
INVALID_PARAMS: 5, | ||
}; | ||
var errorCodes = { ERRORS_FOUND: 1, NO_SOLIUMRC: 3, WRITE_FAILED: 4, INVALID_PARAMS: 5, FILE_NOT_FOUND: 6 }; | ||
@@ -54,5 +50,4 @@ /** | ||
} catch (e) { | ||
console.error ( | ||
'An error occurred while writing to ' + SOLIUMRC_FILENAME_ABSOLUTE + ':\n' + e | ||
); | ||
errorReporter.reportFatal ( | ||
'An error occurred while writing to ' + SOLIUMRC_FILENAME_ABSOLUTE + ':\n' + e.message); | ||
process.exit (errorCodes.WRITE_FAILED); | ||
@@ -63,20 +58,2 @@ } | ||
/** | ||
* Insert any rules that are present in SoliumRules.json but not in user's .soliumrc.json's rules | ||
* @param {Object} userConfig User Configuration object | ||
* @returns {void} | ||
*/ | ||
function synchronizeWithSoliumRules (userConfig) { | ||
Object.keys (soliumRules).filter (function (rulename) { | ||
return soliumRules [rulename].enabled; | ||
}).forEach (function (rulename) { | ||
//only insert those rules that don't already exist. If a rule exists and is disabled, leave it untouched | ||
if (!userConfig.rules.hasOwnProperty (rulename)) { | ||
userConfig.rules [rulename] = true; | ||
} | ||
}); | ||
writeConfigFile (userConfig); | ||
} | ||
/** | ||
* Copy data from cli-utils/.default-solium-ignore to (newly created) .soliumignore in user's root directory | ||
@@ -92,5 +69,4 @@ * @returns {void} | ||
} catch (e) { | ||
console.error ( | ||
'An error occurred while writing to ' + SOLIUMIGNORE_FILENAME_ABSOLUTE + ':\n' + e | ||
); | ||
errorReporter.reportFatal ( | ||
'An error occurred while writing to ' + SOLIUMIGNORE_FILENAME_ABSOLUTE + ':\n' + e.message); | ||
process.exit (errorCodes.WRITE_FAILED); | ||
@@ -105,14 +81,3 @@ } | ||
function createDefaultConfigJSON () { | ||
var config = { | ||
'custom-rules-filename': null, | ||
rules: {} | ||
}; | ||
Object.keys (soliumRules).filter (function (rulename) { | ||
return soliumRules [rulename].enabled; | ||
}).forEach (function (rulename) { | ||
config.rules [rulename] = true; | ||
}); | ||
writeConfigFile (config); | ||
writeConfigFile (require (DEFAULT_SOLIUMRC_PATH)); | ||
} | ||
@@ -126,2 +91,3 @@ | ||
* @param {String} fileName (optional) File name to use when reporting errors | ||
* @returns {Integer} numOfErrors Number of Lint ERRORS that occured. | ||
*/ | ||
@@ -142,13 +108,14 @@ function lintString (sourceCode, userConfig, errorReporter, fileName) { | ||
} catch (e) { | ||
console.error ( | ||
'An error occurred while running the linter on ' + fileName + ':\n' + e.stack | ||
); | ||
return; | ||
errorReporter.reportFatal ( | ||
'An error occurred while linting over ' + fileName + ': ' + e.message); | ||
process.exit (errorCodes.ERRORS_FOUND); | ||
} | ||
//if any lint errors exist, report them | ||
// If any lint/internal errors/warnings exist, report them | ||
lintErrors.length && | ||
errorReporter.report (fileName || '[stdin]', sourceCode, lintErrors, fixesApplied); | ||
errorReporter.report (fileName, sourceCode, lintErrors, fixesApplied); | ||
return lintErrors.length; | ||
return lintErrors.reduce (function (numOfErrors, err) { | ||
return err.type === 'error' ? numOfErrors+1 : numOfErrors; | ||
}, 0); | ||
} | ||
@@ -161,5 +128,6 @@ | ||
* @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) { | ||
var sourceCode = ''; | ||
var sourceCode; | ||
@@ -169,5 +137,4 @@ try { | ||
} catch (e) { | ||
console.error ( | ||
'[ERROR] Unable to read ' + fileName + ': ' + e | ||
); | ||
errorReporter.reportFatal ('Unable to read ' + fileName + ': ' + e.message); | ||
process.exit (errorCodes.FILE_NOT_FOUND); | ||
} | ||
@@ -179,6 +146,7 @@ | ||
/** | ||
* Function that calls Solium object's linter based on user settings | ||
* 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. | ||
* If not given, we lint the entire directory's (and sub-directories') solidity files | ||
* @returns {Integer} totalNumOfErrors Total no. of errors found throughout the codebase (directory) linted. | ||
*/ | ||
@@ -196,12 +164,14 @@ function lint (userConfig, input, ignore, errorReporter) { | ||
if (filesToLint) { | ||
errorCount = sum (filesToLint.map (function (file) { | ||
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. | ||
var sourceCode = fs.readFileSync ('/dev/stdin', 'utf-8'); | ||
errorCount = lintString (sourceCode, userConfig, errorReporter); | ||
userConfig.options.returnInternalIssues = true; | ||
errorCount = lintString (sourceCode, userConfig, errorReporter, '[stdin]'); | ||
} else { | ||
console.error( | ||
'ERROR! Must specify input for linter using --file, --dir or --stdin' | ||
); | ||
errorReporter.reportFatal ('Must specify input for linter using --file, --dir or --stdin'); | ||
process.exit (errorCodes.INVALID_PARAMS); | ||
@@ -211,3 +181,2 @@ } | ||
errorReporter.finalize && errorReporter.finalize(); | ||
return errorCount; | ||
@@ -230,3 +199,2 @@ } | ||
.option ('--watch, --hot', 'Enable Hot Loading (Hot Swapping)') | ||
.option ('-s, --sync', 'Make sure that all Solium rules enabled by default are specified in your ' + SOLIUMRC_FILENAME) | ||
.option ('-R, --reporter <name>', 'Specify the format in which to report the issues found') | ||
@@ -237,15 +205,16 @@ .option ('--fix', 'Fix the errors where possible.'); | ||
/** | ||
* Function that takes a name and returns an error reporter | ||
* 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) { | ||
if (name === 'pretty' || typeof name === 'undefined') { | ||
return require ('./reporters/pretty'); | ||
} else if (name === 'gcc') { | ||
return require ('./reporters/gcc'); | ||
name = name || 'pretty'; | ||
try { | ||
return require ('./reporters/' + name); | ||
} catch (e) { | ||
throw new Error ( | ||
'Invalid reporter "' + name + '". Valid reporters are "gcc" and "pretty"' | ||
); | ||
} | ||
throw new Error ( | ||
'Invalid reporter "' + name + '". Valid reporters are "gcc" or "pretty"' | ||
); | ||
} | ||
@@ -271,3 +240,3 @@ | ||
} catch (e) { | ||
console.error ('ERROR! ' + e.message); | ||
console.error ('[FATAL ERROR] ' + e.message); | ||
process.exit (errorCodes.INVALID_PARAMS) | ||
@@ -279,13 +248,13 @@ } | ||
} catch (e) { | ||
console.error ( | ||
'ERROR! Couldn\'t find ' + SOLIUMRC_FILENAME + ' in the current directory.\nUse solium --init to create one.' | ||
); | ||
// Check if soliumrc file exists. If yes, then the file is in an invalid format. | ||
if (fs.existsSync (SOLIUMRC_FILENAME_ABSOLUTE)) { | ||
errorReporter.reportFatal ('An invalid ' + SOLIUMRC_FILENAME + ' was provided. ' + e.message); | ||
} else { | ||
errorReporter.reportFatal ('Couldn\'t find ' | ||
+ SOLIUMRC_FILENAME + ' in the current directory. Use "solium --init" to create one.'); | ||
} | ||
process.exit (errorCodes.NO_SOLIUMRC); | ||
} | ||
if (cli.sync) { | ||
synchronizeWithSoliumRules (userConfig); | ||
return writeConfigFile (userConfig); | ||
} | ||
//if custom rules' file is set, make sure we have its absolute path | ||
@@ -308,4 +277,4 @@ if ( | ||
} catch (e) { | ||
console.error ( | ||
'There was an error trying to read \'' + SOLIUMIGNORE_FILENAME_ABSOLUTE + '\':\n' + e | ||
errorReporter.reportInternal ( | ||
'There was an error trying to read \'' + SOLIUMIGNORE_FILENAME_ABSOLUTE + '\': ' + e.message | ||
); | ||
@@ -316,11 +285,7 @@ } | ||
if (cli.stdin) { | ||
return console.error ( | ||
'ERROR! Cannot watch files when reading from stdin' | ||
); | ||
return errorReporter.reportFatal ('Cannot watch files when reading from stdin'); | ||
} | ||
if (cli.fix) { | ||
return console.error ( | ||
'ERROR! Autofixing is currently not supported in watch mode.' | ||
); | ||
return errorReporter.reportFatal ('Autofixing is not supported in watch mode.'); | ||
} | ||
@@ -342,8 +307,4 @@ } | ||
} else { | ||
if (errorCount > 0) { | ||
process.exit(errorCodes.ERRORS_FOUND); | ||
} | ||
} else if (errorCount > 0) { | ||
process.exit (errorCodes.ERRORS_FOUND); | ||
} | ||
@@ -350,0 +311,0 @@ } |
@@ -10,4 +10,27 @@ /** | ||
reportFatal: function (message) { | ||
console.log ('[FATAL ERROR] ' + message); | ||
}, | ||
reportInternal: function (message) { | ||
console.log ('[WARNING] ' + message); | ||
}, | ||
report: function (filename, sourceCode, lintErrors, fixesApplied) { | ||
var internalIssuesExist = false; | ||
// Examine internal issues first | ||
lintErrors.forEach (function (issue, index) { | ||
if (!issue.internal) { | ||
return; | ||
} | ||
console.log (issue.message); | ||
delete lintErrors [index]; | ||
internalIssuesExist = true; | ||
}); | ||
internalIssuesExist && console.log ('\n'); | ||
lintErrors.forEach (function (error) { | ||
@@ -14,0 +37,0 @@ |
@@ -20,2 +20,6 @@ /** | ||
function colorInternalIssue (type) { | ||
return type === 'warning' ? 'blue' : 'red'; | ||
} | ||
var counts = {}; | ||
@@ -25,9 +29,35 @@ | ||
reportFatal: function (message) { | ||
console.log (('[FATAL ERROR] ' + message) [colorInternalIssue ('error')]); | ||
}, | ||
// Convenience method when only a message needs to be passed as part of an internal issue | ||
reportInternal: function (message) { | ||
console.log (('[WARNING] ' + message) [colorInternalIssue ('warning')]); | ||
}, | ||
report: function (filename, sourceCode, lintErrors, fixesApplied) { | ||
var lines = sourceCode.split ('\n'); | ||
var errorLine, line, tabCount; | ||
var internalIssuesExist = false; | ||
// First output all internal issues | ||
lintErrors.forEach (function (issue, index) { | ||
if (!issue.internal) { | ||
return; | ||
} | ||
console.log (issue.message [colorInternalIssue (issue.type)]); | ||
// Remove internal issue, so only rule errors reach the next loop | ||
delete lintErrors [index]; | ||
internalIssuesExist = true; | ||
}); | ||
internalIssuesExist && console.log ('\n'); | ||
console.log(('\nIn ' + filename + ':').bold); | ||
lintErrors.forEach (function (error) { | ||
if (error.line != errorLine) { | ||
console.log(('\nIn ' + filename + ', line ' + error.line + ':').bold); | ||
console.log(('\nLine ' + error.line + ':').bold); | ||
@@ -59,3 +89,3 @@ line = lines[error.line - 1]; | ||
if (Array.isArray (fixesApplied)) { | ||
counts.fixes = fixesApplied.length; | ||
counts.fixes = (counts.fixes || 0) + fixesApplied.length; | ||
} | ||
@@ -62,0 +92,0 @@ |
@@ -8,7 +8,7 @@ /** | ||
var jsUtils = require ('./utils/js-utils'); | ||
var util = require ('util'), | ||
isErrObjectValid = require ('../config/schemas/error-reported-by-rule').validationFunc; | ||
var INHERITABLE_METHODS = [ | ||
'getSourceCode', | ||
'on' | ||
'getSourceCode' | ||
]; | ||
@@ -26,2 +26,5 @@ | ||
// Set contect attribute 'options' iff options were provided. | ||
ruleDesc.options && Object.assign (contextObject, { options: ruleDesc.options }); | ||
//set read-only properties of the context object | ||
@@ -55,9 +58,8 @@ Object.defineProperties (contextObject, { | ||
if (!jsUtils.isStrictlyObject (error)) { | ||
throw new Error (JSON.stringify (error) + ' is an invalid Error object'); | ||
if (!isErrObjectValid (error)) { | ||
throw new Error ('Rule "' + ruleName + | ||
'": invalid error object was passed. AJV message:\n' + util.inspect (isErrObjectValid.errors)); | ||
} | ||
Object.assign (error, { ruleName: ruleName, | ||
ruleMeta: ruleMeta, type: contextObject.meta.type }); | ||
Object.assign (error, { ruleName: ruleName, ruleMeta: ruleMeta, type: contextObject.meta.type }); | ||
Solium.report (error); | ||
@@ -64,0 +66,0 @@ |
182
lib/rules.js
@@ -10,10 +10,24 @@ /** | ||
path = require ('path'), | ||
util = require ('util'), | ||
jsUtils = require ('./utils/js-utils'), | ||
ruleLoader = require ('./utils/rule-loader'), | ||
soliumRules = require ('../config/solium').rules, //list of all rules available inside solium | ||
rules = {}; | ||
var RULES_DIR = path.join (__dirname, 'rules'), | ||
var loadRulesFromUpstream = ruleLoader.loadRulesFromUpstream; | ||
var RULES_DIR = path.join (__dirname, ruleLoader.constants.SOLIUM_CORE_RULES_DIRNAME), | ||
CWD = process.cwd (), | ||
JS_EXT = '.js'; | ||
// Utilities for getRuleSeverity() | ||
var configSchema = require ('../config/schemas/config'), | ||
SchemaValidator = new require ('ajv') ({ allErrors: true }), | ||
severityValueSchemas = configSchema.properties.rules.patternProperties ['^.+$'].oneOf; | ||
var isValidSeverityString = SchemaValidator.compile (severityValueSchemas [0]), | ||
isValidSeverityInt = SchemaValidator.compile (severityValueSchemas [1]), | ||
isValidSeverityArray = SchemaValidator.compile (severityValueSchemas [2]), | ||
isAValidPlugin = require ('../config/schemas/plugin').validationFunc; | ||
module.exports = { | ||
@@ -31,2 +45,3 @@ | ||
* load the user-specified rules from the rules/ directory and custom rules from specified file | ||
* This function loads rule information from a DEPRECATED config format. | ||
* @param {Object} userRules object whose keys specify the rule name and boolean values specify whether to include it | ||
@@ -36,4 +51,4 @@ * @param {String} customRulesFilePath The file from where definitions of user-defined rules are loaded | ||
*/ | ||
load: function (userRules, customRulesFilePath, noReset) { | ||
var ruleFiles, idCounter = 1, customRules = {}; | ||
loadUsingDeprecatedConfigFormat: function (userRules, customRulesFilePath, noReset) { | ||
var ruleFiles, idCounter = 1; | ||
@@ -47,17 +62,5 @@ if (!jsUtils.isStrictlyObject (userRules)) { | ||
} catch (e) { | ||
throw new Error ('Unable to read ' + RULES_DIR + ':\n' + e.stack); | ||
throw new Error ('Unable to read ' + RULES_DIR + ': ' + e.message); | ||
} | ||
if (customRulesFilePath && typeof customRulesFilePath === 'string') { | ||
if (!path.isAbsolute (customRulesFilePath)) { | ||
customRulesFilePath = path.join (CWD, customRulesFilePath); | ||
} | ||
try { | ||
customRules = require (customRulesFilePath); | ||
} catch (e) { | ||
throw new Error ('Unable to read ' + customRulesFilePath + ':\n' + e); | ||
} | ||
} | ||
!noReset && this.reset (); | ||
@@ -73,3 +76,3 @@ | ||
} catch (e) { | ||
throw new Error ('Unable to read ' + absoluteRuleFilePath + ':\n' + e); | ||
throw new Error ('Unable to read ' + absoluteRuleFilePath + ': ' + e.message); | ||
} | ||
@@ -99,41 +102,105 @@ } | ||
Object.keys (customRules).forEach (function (ruleName) { | ||
//skip if user has not enabled custom rule ruleName | ||
if (!userRules [ruleName]) { | ||
return; | ||
//if there is still a rule set to true and not yet expanded, its an invalid rule (neither built-in nor custom) | ||
Object.keys (userRules).forEach (function (ruleName) { | ||
if (typeof userRules [ruleName] !== 'object') { | ||
throw new Error ('Rule ' + ruleName + ' was not found'); | ||
} | ||
}); | ||
if (rules [ruleName]) { | ||
/** | ||
* Gets executed when a pre-defined and a custom rule have a name clash. | ||
* The custom rule over-writes the pre-defined one. | ||
*/ | ||
return userRules; | ||
}, | ||
/** | ||
* Load Solium rules as described in the configuration object provided. | ||
* @param {Object} config The configuration object (read from soliumrc) that describes what rules the user wishes to apply. | ||
* @param {Boolean} noReset Determines whether to re-initilize internal variables or not. If this param has a false-equivalent value, data is reset. | ||
* @returns {Object} userRules Definitions of all user-requested rules. Throws error if a rule in userRules is not amongst available rules | ||
*/ | ||
load: function (config, noReset) { | ||
var ruleDescriptions = {}, ruleConfigs = {}, getRuleSeverity = this.getRuleSeverity; | ||
!noReset && this.reset (); | ||
// If plugins are passed, ensure all of them are installed in the same scope as Solium. | ||
// If not, provide appropriate error messages, instructions & doc links. | ||
if (config.plugins && config.plugins.length) { | ||
config.plugins.forEach (function (pName) { | ||
// User must only provide the plugin name, not the solium plugin prefix string | ||
var plugin; | ||
pName = ruleLoader.constants.SOLIUM_PLUGIN_PREFIX + pName; | ||
try { | ||
plugin = require (pName); | ||
} catch (e) { | ||
if (e.code === 'MODULE_NOT_FOUND') { | ||
// Plugin is not installed in Solium's scope | ||
throw new Error ( | ||
'Oops! The Plugin "' + pName + '" was not found.' + | ||
'\n\nPlease make sure that it is installed globally by running "npm install -g ' + pName + '"' | ||
); | ||
} | ||
// Some other error | ||
throw new Error ( | ||
'Oops! An error occured while trying to load the plugin "' + | ||
pName + '".' + e.message + | ||
'\n\nPlease see http://solium.readthedocs.io/en/latest/user-guide.html#plugins for plugin usage.' | ||
); | ||
} | ||
// If plugin was loaded successfully, validate it using schema. | ||
if (!isAValidPlugin (plugin)) { | ||
throw new Error ( | ||
'"' + pName + '" is not a valid plugin.' + | ||
'\nPlease see http://solium.readthedocs.io/en/latest/developer-guide.html#developing-a-plugin' + | ||
' for plugin development. AJV Message:\n' + util.inspect (isAValidPlugin.errors) | ||
); | ||
} | ||
}); | ||
} | ||
if (config.extends) { | ||
try { | ||
Object.assign (ruleConfigs, ruleLoader.resolveUpstream (config.extends)); | ||
} catch (e) { | ||
throw new Error ( | ||
'An error occured while trying to resolve dependancy "' + config.extends + '": ' + e.message); | ||
} | ||
} | ||
rules [ruleName] = { | ||
verify: customRules [ruleName] | ||
}; | ||
// If both extends & rules attributes exist, the rules imported from "rules" attr will override any rules | ||
// imported from "extends" in case of a name clash. | ||
if (config.rules && Object.keys (config.rules).length) { | ||
Object.assign (ruleConfigs, config.rules); | ||
} | ||
//in case of rule name clash, assign the custom rule the same id as the pre-defined rule with the same name | ||
userRules [ruleName] = { | ||
enabled: true, | ||
custom: true, | ||
type: 'custom-error', | ||
id: ( | ||
userRules [ruleName] && userRules [ruleName].id ? | ||
userRules [ruleName].id : idCounter++ | ||
) | ||
// Remove all rules that are disabled. | ||
Object.keys (ruleConfigs).forEach (function (name) { | ||
getRuleSeverity (ruleConfigs [name]) < 1 && delete ruleConfigs [name]; | ||
}); | ||
// Load all enabled rules. | ||
try { | ||
Object.assign (rules, ruleLoader.load (Object.keys (ruleConfigs))); | ||
} catch (e) { | ||
throw new Error ('An error occured while trying to load rules: ' + e.message); | ||
} | ||
// Use rule definitions & configs to generate ruleDescriptions | ||
Object.keys (rules).forEach (function (name) { | ||
var desc = { | ||
description: rules [name].meta.docs.description, | ||
recommended: rules [name].meta.docs.recommended, | ||
type: (getRuleSeverity (ruleConfigs [name]) === 1) ? 'warning' : 'error' | ||
}; | ||
}); | ||
// Only set "options" attribute if the rule config is an array of length is at least 2. | ||
if (Array.isArray (ruleConfigs [name]) && ruleConfigs [name].length > 1) { | ||
desc.options = ruleConfigs [name].slice (1); | ||
} | ||
//if there is still a rule set to true and not yet expanded, its an invalid rule (neither built-in nor custom) | ||
Object.keys (userRules).forEach (function (ruleName) { | ||
if (typeof userRules [ruleName] !== 'object') { | ||
throw new Error ('Rule ' + ruleName + ' was not found'); | ||
} | ||
ruleDescriptions [name] = desc; | ||
}); | ||
return userRules; | ||
return ruleDescriptions; | ||
}, | ||
@@ -151,4 +218,27 @@ | ||
} | ||
}, | ||
/** | ||
* Get severity value for a rule from its given configuration description. | ||
* @param {Integer|String|Array} ruleConfig configuration for the rule (picked up from soliumrc) | ||
* @returns {Integer} severity Either 0 (rule turned off), 1 (warning) or 2 (error). | ||
*/ | ||
getRuleSeverity: function getRuleSeverity (ruleConfig) { | ||
if (isValidSeverityInt (ruleConfig)) { | ||
return ruleConfig; | ||
} | ||
if (isValidSeverityString (ruleConfig)) { | ||
return ({ | ||
'off': 0, 'warning': 1, 'error': 2 | ||
}) [ruleConfig]; | ||
} | ||
if (isValidSeverityArray (ruleConfig)) { | ||
return getRuleSeverity (ruleConfig [0]); | ||
} | ||
throw new Error ('Invalid configuration value for rule.'); | ||
} | ||
}; |
@@ -10,7 +10,21 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Ensure that array declarations don\'t have space between the type and brackets' | ||
}, | ||
schema: [], | ||
fixable: 'whitespace' | ||
}, | ||
create: function (context) { | ||
var sourceCode = context.getSourceCode (); | ||
context.on ('Type', function (emitted) { | ||
function inspectType (emitted) { | ||
var node = emitted.node; | ||
@@ -22,28 +36,53 @@ | ||
var code = sourceCode.getText (node), | ||
isSpaceBetweenLiteralAndBracket = !/\w/.test ( code.slice (code.indexOf ('[')-1, code.indexOf ('[')) ); | ||
var code = sourceCode.getText (node), whitespaceDetector = /\s*(?=\[)/; | ||
isSpaceBetweenLiteralAndBracket && context.report ({ | ||
// First the regex must detect whitespace between literal and opening bracket | ||
var scanned = whitespaceDetector.exec (code), | ||
whitespaceString = scanned [0], index = scanned.index; | ||
whitespaceString !== '' && context.report ({ | ||
node: node, | ||
location: { column: index + 1 }, | ||
message: ( | ||
'There should be no whitespace between literal ' + | ||
(typeof node.literal === 'string' ? node.literal + ' ' : '') + | ||
'and \'[]\'' | ||
) | ||
'There should be no whitespace between "' | ||
+ code.slice (0, index) + '" and the opening square bracket.' | ||
), | ||
fix: function (fixer) { | ||
return [fixer.removeRange ( | ||
[node.start + index, node.start + index + whitespaceString.length])]; | ||
} | ||
}); | ||
if ( | ||
node.array_parts.length === 1 && | ||
node.array_parts [0] === null && | ||
code.slice (code.indexOf ('[')) !== '[]' | ||
) { | ||
context.report ({ | ||
// Next, the regex must detect whitespace between opening & closing brackets | ||
// Since this regex doesn't exclude the brackets themselves, we have to move positions to acquire | ||
// the right info. | ||
if (node.array_parts.length === 1 && node.array_parts [0] === null) { | ||
var bracketString, indexWs; | ||
whitespaceDetector = /\[(\s*)\]/; | ||
scanned = whitespaceDetector.exec (code); | ||
bracketString = scanned [0], whitespaceString = scanned [1], index = scanned.index, indexWs = index + 1; | ||
(whitespaceString !== '') && context.report ({ | ||
node: node, | ||
message: 'There should be no whitespace between square brackets. Use [] instead.' | ||
location: { column: indexWs + 1 }, | ||
fix: function (fixer) { | ||
return [fixer.replaceTextRange ( | ||
[node.start + index, node.start + index + bracketString.length], '[]')]; | ||
}, | ||
message: 'There should be no whitespace between opening & closing square brackets. Use [] instead.' | ||
}); | ||
} | ||
}); | ||
} | ||
return { | ||
Type: inspectType | ||
}; | ||
} | ||
}; | ||
}; |
@@ -10,4 +10,16 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure that there is exactly a 2-line gap between Contract and Funtion declarations' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var sourceCode = context.getSourceCode (), | ||
@@ -18,3 +30,3 @@ noCodeRegExp = /^[ \t]*$/, | ||
context.on ('Program', function (emitted) { | ||
function inspectProgram (emitted) { | ||
var node = emitted.node, body = node.body; | ||
@@ -54,3 +66,3 @@ | ||
} | ||
}); | ||
} | ||
@@ -95,8 +107,15 @@ | ||
topLevelDeclarations.forEach (function (event) { | ||
context.on (event, inspectChild) | ||
var listeners = { | ||
Program: inspectProgram | ||
}; | ||
topLevelDeclarations.forEach (function (node) { | ||
listeners [node] = inspectChild; | ||
}); | ||
return listeners; | ||
} | ||
}; |
@@ -10,6 +10,18 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure that contract, library, modifier and struct names follow CamelCase notation' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var camelCaseRegEx = /^([A-Z][A-Za-z0-9]+)+$/; | ||
var eventsToWatch = { | ||
var nodesToWatch = { | ||
'ContractStatement': 'Contract', | ||
@@ -21,4 +33,4 @@ 'LibraryStatement': 'Library', | ||
function inspectEventsToWatch (event) { | ||
context.on (event, function (emitted) { | ||
function createInspector (nodeDesc) { | ||
return (function inspect (emitted) { | ||
var node = emitted.node; | ||
@@ -33,3 +45,3 @@ | ||
node: node, | ||
message: eventsToWatch [event] + ' name \'' + node.name + '\' doesn\'t follow the CamelCase notation.' | ||
message: nodeDesc + ' name \'' + node.name + '\' doesn\'t follow the CamelCase notation.' | ||
}); | ||
@@ -40,6 +52,11 @@ } | ||
Object.keys (eventsToWatch).forEach (inspectEventsToWatch); | ||
return Object.keys (nodesToWatch).reduce (function (listeners, nodeName) { | ||
listeners [nodeName] = createInspector (nodesToWatch [nodeName]); | ||
return listeners; | ||
}, {}); | ||
} | ||
}; | ||
}; |
@@ -14,6 +14,19 @@ /** | ||
module.exports = { | ||
verify: function (context) { | ||
context.on ('CallExpression', function (emittedObject) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Suggest replacing deprecated \'suicide\' for \'selfdestruct\'' | ||
}, | ||
schema: [], | ||
fixable: 'code' | ||
}, | ||
create: function (context) { | ||
function inspectCallExpression (emittedObject) { | ||
if (!emittedObject.exit) { | ||
@@ -29,10 +42,16 @@ return; | ||
node: emittedObject.node, | ||
message: "'suicide' is deprecated. Use 'selfdestruct' instead", | ||
fix: function (fixer) { | ||
return [fixer.replaceTextRange ([callee.start, callee.end], 'selfdestruct')]; | ||
}, | ||
message: '\'suicide\' is deprecated. Use \'selfdestruct\' instead.' | ||
}); | ||
} | ||
} | ||
}); | ||
return { | ||
CallExpression: inspectCallExpression | ||
}; | ||
} | ||
} | ||
} | ||
}; |
@@ -26,4 +26,18 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Ensure that string are quoted with double-quotes only', | ||
replacedBy: ['quotes'] | ||
}, | ||
schema: [], | ||
deprecated: true | ||
}, | ||
create: function (context) { | ||
var doubleQuotesLiteralRegExp = /^\".*\"$/, | ||
@@ -50,6 +64,8 @@ sourceCode = context.getSourceCode (); | ||
context.on ('Literal', inspectLiteral); | ||
return { | ||
Literal: inspectLiteral | ||
}; | ||
} | ||
}; | ||
}; |
@@ -34,4 +34,16 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Ensure that all import statements are on top of the file' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
function inspectImportStatement (emitted) { | ||
@@ -65,3 +77,5 @@ var node = emitted.node, | ||
context.on ('ImportStatement', inspectImportStatement); | ||
return { | ||
ImportStatement: inspectImportStatement | ||
}; | ||
@@ -68,0 +82,0 @@ } |
@@ -14,17 +14,45 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
var sourceCode = context.getSourceCode (); | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure consistent indentation of 4 spaces per level' | ||
}, | ||
var BASE_INDENTATION_STYLE = ' ', | ||
BASE_INDENTATION_STYLE_DESC = '4 SPACES', | ||
BASE_INDENTATION_STYLE_REGEXP_GLOBAL = new RegExp (BASE_INDENTATION_STYLE, 'g'); | ||
schema: [{ | ||
oneOf: [ | ||
{ type: 'string', enum: ['tab'] }, | ||
{ type: 'integer', minimum: 0 } | ||
] | ||
}] | ||
var topLevelDeclarations = [ | ||
'ContractStatement', | ||
'LibraryStatement' | ||
]; | ||
}, | ||
create: function (context) { | ||
// default configurations | ||
var BASE_INDENTATION_STYLE = ' '.repeat (4), | ||
BASE_INDENTATION_STYLE_DESC = '4 SPACES'; | ||
if (context.options) { | ||
if (context.options [0] === 'tab') { | ||
BASE_INDENTATION_STYLE = '\t'; | ||
BASE_INDENTATION_STYLE_DESC = '1 TAB'; | ||
} else { | ||
var spCount = context.options [0]; | ||
BASE_INDENTATION_STYLE = ' '.repeat (spCount); | ||
BASE_INDENTATION_STYLE_DESC = spCount + ' SPACE' + (spCount === 1 ? '' : 'S'); // grammar-nazi-proof error messages | ||
} | ||
} | ||
var BASE_INDENTATION_STYLE_REGEXP_GLOBAL = new RegExp (BASE_INDENTATION_STYLE, 'g'); | ||
var sourceCode = context.getSourceCode (); | ||
var topLevelDeclarations = ['ContractStatement', 'LibraryStatement']; | ||
//Ensure NO indentation exists before top-level declarations (contract, library) | ||
context.on ('Program', function (emitted) { | ||
function inspectProgram (emitted) { | ||
var node = emitted.node; | ||
@@ -80,3 +108,3 @@ | ||
node.body.forEach (inspectProgramChild); | ||
}); | ||
} | ||
@@ -86,50 +114,46 @@ | ||
//Ensure level 1 indentation before all immediate children of top-level declarations | ||
topLevelDeclarations.forEach (function (event) { | ||
function inspectTopLevelDeclaration (emitted) { | ||
var body = emitted.node.body || [], | ||
levelOneIndentRegExp = new RegExp ('^\\n' + BASE_INDENTATION_STYLE + '$'), | ||
endingLineRegExp = new RegExp ('^' + BASE_INDENTATION_STYLE + '(\\S| *)$'); //either a non-whitespace character or 1 extra whitespace followed by * (closing of multi-line comment) | ||
context.on (event, function (emitted) { | ||
var body = emitted.node.body || [], | ||
levelOneIndentRegExp = new RegExp ('^\\n' + BASE_INDENTATION_STYLE + '$'), | ||
endingLineRegExp = new RegExp ('^' + BASE_INDENTATION_STYLE + '(\\S| *)$'); //either a non-whitespace character or 1 extra whitespace followed by * (closing of multi-line comment) | ||
if (emitted.exit) { | ||
return; | ||
} | ||
if (emitted.exit) { | ||
return; | ||
function inspectChild (child) { | ||
var prevChars = sourceCode.getPrevChars (child, BASE_INDENTATION_STYLE.length+1), | ||
endingLineNum = sourceCode.getEndingLine (child); | ||
//if the start of node doesn't follow correct indentation | ||
if (!levelOneIndentRegExp.test (prevChars)) { | ||
context.report ({ | ||
node: child, | ||
message: 'Incorrect indentation: Make sure you use ' + BASE_INDENTATION_STYLE_DESC + ' per level and don\'t precede the code by any comments.' | ||
}); | ||
} | ||
function inspectChild (child) { | ||
var prevChars = sourceCode.getPrevChars (child, BASE_INDENTATION_STYLE.length+1), | ||
endingLineNum = sourceCode.getEndingLine (child); | ||
//if the start of node doesn't follow correct indentation | ||
if (!levelOneIndentRegExp.test (prevChars)) { | ||
context.report ({ | ||
node: child, | ||
message: 'Incorrect indentation: Make sure you use ' + BASE_INDENTATION_STYLE_DESC + ' per level and don\'t precede the code by any comments.' | ||
}); | ||
} | ||
//if the node doesn't end on the same line as it starts and the ending line doesn't follow correct indentation | ||
if ( | ||
sourceCode.getLine (child) !== endingLineNum && | ||
!endingLineRegExp.test (sourceCode.getTextOnLine (endingLineNum).slice (0, 5)) | ||
) { | ||
context.report ({ | ||
node: child, | ||
location: { | ||
line: endingLineNum, | ||
column: 0 | ||
}, | ||
message: 'Incorrect indentation: Make sure you use ' + BASE_INDENTATION_STYLE_DESC + ' per level and don\'t precede the code by any comments.' | ||
}); | ||
} | ||
//if the node doesn't end on the same line as it starts and the ending line doesn't follow correct indentation | ||
if ( | ||
sourceCode.getLine (child) !== endingLineNum && | ||
!endingLineRegExp.test (sourceCode.getTextOnLine (endingLineNum).slice (0, 5)) | ||
) { | ||
context.report ({ | ||
node: child, | ||
location: { | ||
line: endingLineNum, | ||
column: 0 | ||
}, | ||
message: 'Incorrect indentation: Make sure you use ' + BASE_INDENTATION_STYLE_DESC + ' per level and don\'t precede the code by any comments.' | ||
}); | ||
} | ||
} | ||
body.forEach (inspectChild); | ||
}); | ||
body.forEach (inspectChild); | ||
} | ||
}); | ||
//Ensure 1 extra indentation inside Block than before it | ||
context.on ('BlockStatement', function (emitted) { | ||
function inspectBlockStatement (emitted) { | ||
var node = emitted.node; | ||
@@ -200,7 +224,7 @@ | ||
node.body.forEach (inspectBlockItem.bind (null, blockIndent)); | ||
}); | ||
} | ||
context.on ('StructDeclaration', function (emitted) { | ||
function inspectStructDeclaration (emitted) { | ||
var node = emitted.node, | ||
@@ -210,3 +234,2 @@ body = node.body || [], | ||
var structDeclarationLineText, currentIndent, currentIndentLevel, structIndent; | ||
var MAX_ATTR_IN_SINGLE_LINE = 3; | ||
@@ -230,8 +253,2 @@ function inspectStructAttr (structIndent, attr) { | ||
if (sourceCode.getLine (node) === endingLineNum) { | ||
if (body.length > MAX_ATTR_IN_SINGLE_LINE) { | ||
context.report ({ | ||
node: node, | ||
message: '\'' + node.name + '\': In case of more than 3 properties, struct declaration needs to be spread over multiple lines with 1 property per line.' | ||
}); | ||
} | ||
return; | ||
@@ -257,11 +274,10 @@ } | ||
}); | ||
} | ||
context.on ('ArrayExpression', function (emitted) { | ||
function inspectArrayExpression (emitted) { | ||
var node = emitted.node, elements = node.elements; | ||
var endingLineNum = sourceCode.getEndingLine (node), | ||
arrayExpressionLineText, currentIndent, currentIndentLevel, arrayIndent; | ||
var MAX_ELEMS_IN_SINGLE_LINE = 3; | ||
@@ -284,8 +300,2 @@ function inspectElement (arrayIndent, elem) { | ||
if (sourceCode.getLine (node) === endingLineNum) { | ||
if (elements.length > MAX_ELEMS_IN_SINGLE_LINE) { | ||
context.report ({ | ||
node: node, | ||
message: 'In case of more than 3 elements, array expression needs to be spread over multiple lines with 1 element per line.' | ||
}); | ||
} | ||
return; | ||
@@ -310,9 +320,8 @@ } | ||
elements.forEach (inspectElement.bind (null, arrayIndent)); | ||
}); | ||
} | ||
//function params (if on multiple lines) | ||
context.on ('FunctionDeclaration', function (emitted) { | ||
function inspectFunctionDeclaration (emitted) { | ||
var node = emitted.node, params = node.params || []; | ||
var MAX_PARAMS_ON_SINGLE_LINE = 3; | ||
@@ -339,8 +348,2 @@ var startLine = sourceCode.getLine (node), | ||
if (startLine === lastArgLine) { | ||
if (params.length > MAX_PARAMS_ON_SINGLE_LINE) { | ||
context.report ({ | ||
node: node, | ||
message: 'Function \'' + node.name + '\': in case of more than 3 parameters, drop each into its own line.' | ||
}); | ||
} | ||
return; | ||
@@ -366,6 +369,6 @@ } | ||
params.forEach (inspectParam.bind (null, paramIndent)); | ||
}); | ||
} | ||
context.on ('CallExpression', function (emitted) { | ||
function inspectCallExpression (emitted) { | ||
@@ -375,3 +378,2 @@ var node = emitted.node; | ||
callExpressionLineText, currentIndent, currentIndentLevel, argIndent; | ||
var MAX_ARGS_ON_SINGLE_LINE = 3; | ||
@@ -394,8 +396,2 @@ function inspectArgument (argIndent, argument) { | ||
if (sourceCode.getLine (node) === endingLineNum) { | ||
if (node.arguments.length > MAX_ARGS_ON_SINGLE_LINE) { | ||
context.report ({ | ||
node: node, | ||
message: 'Function \'' + node.callee.name + '\': in case of more than 3 arguments, drop each into its own line.' | ||
}); | ||
} | ||
return; | ||
@@ -422,6 +418,22 @@ } | ||
} | ||
var response = { | ||
CallExpression: inspectCallExpression, | ||
FunctionDeclaration: inspectFunctionDeclaration, | ||
ArrayExpression: inspectArrayExpression, | ||
StructDeclaration: inspectStructDeclaration, | ||
BlockStatement: inspectBlockStatement, | ||
Program: inspectProgram | ||
}; | ||
topLevelDeclarations.forEach (function (nodeName) { | ||
response [nodeName] = inspectTopLevelDeclaration; | ||
}); | ||
return response; | ||
} | ||
}; |
@@ -11,7 +11,19 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Ensure that every if, for, while and do statement is followed by an opening curly brace \'{\' on the same line' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var sourceCode = context.getSourceCode (); | ||
context.on ('IfStatement', function (emitted) { | ||
function inspectIfStatement (emitted) { | ||
var node = emitted.node; | ||
@@ -167,6 +179,6 @@ | ||
}); | ||
}); | ||
} | ||
context.on ('ForStatement', function (emitted) { | ||
function inspectForStatement (emitted) { | ||
var node = emitted.node, | ||
@@ -212,6 +224,6 @@ lastNodeOnStartingLine = node.update || node.test || node.init; | ||
} | ||
}); | ||
} | ||
context.on ('WhileStatement', function (emitted) { | ||
function inspectWhileStatement (emitted) { | ||
var node = emitted.node, | ||
@@ -251,6 +263,6 @@ startingLine = sourceCode.getLine (node); | ||
} | ||
}); | ||
} | ||
context.on ('DoWhileStatement', function (emitted) { | ||
function inspectDoWhileStatement (emitted) { | ||
var node = emitted.node, | ||
@@ -290,3 +302,3 @@ startingLine = sourceCode.getLine (node); | ||
} | ||
}); | ||
} | ||
@@ -298,3 +310,3 @@ | ||
*/ | ||
context.on ('WithStatement', function (emitted) { | ||
function inspectWithStatement (emitted) { | ||
var node = emitted.node; | ||
@@ -326,6 +338,6 @@ | ||
} | ||
}); | ||
} | ||
context.on ('StructDeclaration', function (emitted) { | ||
function inspectStructDeclaration (emitted) { | ||
var node = emitted.node, | ||
@@ -342,6 +354,6 @@ code = sourceCode.getText (node).replace ('struct ' + node.name, ''); | ||
}); | ||
}); | ||
} | ||
context.on ('ContractStatement', function (emitted) { | ||
function inspectContractStatement (emitted) { | ||
var node = emitted.node, | ||
@@ -358,6 +370,6 @@ code = sourceCode.getTextOnLine (sourceCode.getLine (node)); | ||
}); | ||
}); | ||
} | ||
context.on ('LibraryStatement', function (emitted) { | ||
function inspectLibraryStatement (emitted) { | ||
var node = emitted.node, | ||
@@ -374,6 +386,6 @@ code = sourceCode.getTextOnLine (sourceCode.getLine (node)); | ||
}); | ||
}); | ||
} | ||
context.on ('FunctionDeclaration', function (emitted) { | ||
function inspectFunctionDeclaration (emitted) { | ||
var node = emitted.node; | ||
@@ -470,5 +482,16 @@ | ||
}); | ||
} | ||
}); | ||
return { | ||
FunctionDeclaration: inspectFunctionDeclaration, | ||
LibraryStatement: inspectLibraryStatement, | ||
ContractStatement: inspectContractStatement, | ||
StructDeclaration: inspectStructDeclaration, | ||
WithStatement: inspectWithStatement, | ||
DoWhileStatement: inspectDoWhileStatement, | ||
WhileStatement: inspectWhileStatement, | ||
ForStatement: inspectForStatement, | ||
IfStatement: inspectIfStatement | ||
}; | ||
@@ -475,0 +498,0 @@ } |
@@ -10,4 +10,16 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure that all variable, function and parameter names follow the mixedCase naming convention' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var mixedCaseRegEx = /^_?[a-z][a-zA-Z0-9]*_?$/; | ||
@@ -26,26 +38,25 @@ var similarNodes = [ | ||
similarNodes.forEach (function (event) { | ||
context.on (event, function (emitted) { | ||
var node = emitted.node; | ||
function inspectFuncOrModif (emitted) { | ||
var node = emitted.node; | ||
/** | ||
* If node's parent is contract / library and node is either a modifier (which means Inheritance), do not apply mixedcase | ||
*/ | ||
if ( | ||
emitted.exit || | ||
( | ||
(node.parent.type === 'ContractStatement' || node.parent.type === 'LibraryStatement') && | ||
(node.type === 'FunctionDeclaration' && node.parent.name === node.name) | ||
) | ||
) { | ||
return; | ||
} | ||
/** | ||
* If node's parent is contract / library and node is either a modifier (which means Inheritance), | ||
* do not apply mixedcase | ||
*/ | ||
if ( | ||
emitted.exit || | ||
( | ||
(node.parent.type === 'ContractStatement' || node.parent.type === 'LibraryStatement') && | ||
(node.type === 'FunctionDeclaration' && node.parent.name === node.name) | ||
) | ||
) { | ||
return; | ||
} | ||
if (!mixedCaseRegEx.test (node.name)) { | ||
report (node, node.name); | ||
} | ||
}); | ||
}); | ||
if (!mixedCaseRegEx.test (node.name)) { | ||
report (node, node.name); | ||
} | ||
} | ||
context.on ('VariableDeclarator', function (emitted) { | ||
function inspectVariableDeclarator (emitted) { | ||
var node = emitted.node; | ||
@@ -63,5 +74,5 @@ | ||
} | ||
}); | ||
} | ||
context.on ('DeclarativeExpression', function (emitted) { | ||
function inspectDeclarativeExpression (emitted) { | ||
var node = emitted.node; | ||
@@ -76,5 +87,5 @@ | ||
} | ||
}); | ||
} | ||
context.on ('InformalParameter', function (emitted) { | ||
function inspectInformalParameter (emitted) { | ||
var node = emitted.node; | ||
@@ -86,9 +97,27 @@ | ||
if (!mixedCaseRegEx.test (node.id)) { | ||
/** | ||
* node.is could either be an object containing "name" or null. | ||
* It is null when there is no name (eg- `function foo() returns(bool) {}`) | ||
* Here, bool's node will have "literal" object but "id" as null. | ||
*/ | ||
if (node.id && !mixedCaseRegEx.test (node.id)) { | ||
report (node, node.id); | ||
} | ||
} | ||
var response = { | ||
InformalParameter: inspectInformalParameter, | ||
DeclarativeExpression: inspectDeclarativeExpression, | ||
VariableDeclarator: inspectVariableDeclarator | ||
}; | ||
similarNodes.forEach (function (nodeName) { | ||
response [nodeName] = inspectFuncOrModif; | ||
}); | ||
return response; | ||
} | ||
}; | ||
}; |
@@ -10,4 +10,16 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure that no empty blocks {} exist' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
function report (node, loc) { | ||
@@ -23,19 +35,16 @@ context.report ({ | ||
similarNodeTypes.forEach (function (event) { | ||
context.on (event, function (emitted) { | ||
var node = emitted.node, | ||
sourceCode = context.getSourceCode (), | ||
text = sourceCode.getText (node); | ||
function inspectCLI (emitted) { | ||
var node = emitted.node, | ||
sourceCode = context.getSourceCode (), text = sourceCode.getText (node); | ||
if (emitted.exit) { | ||
return; | ||
} | ||
if (emitted.exit) { | ||
return; | ||
} | ||
if (!node.body.length) { | ||
report (node, { column: text.indexOf ('{') }); | ||
} | ||
}); | ||
}); | ||
if (!node.body.length) { | ||
report (node, { column: text.indexOf ('{') }); | ||
} | ||
} | ||
context.on ('BlockStatement', function (emitted) { | ||
function inspectBlockStatement (emitted) { | ||
var node = emitted.node; | ||
@@ -54,6 +63,17 @@ | ||
} | ||
} | ||
var response = { | ||
BlockStatement: inspectBlockStatement | ||
}; | ||
similarNodeTypes.forEach (function (nodeName) { | ||
response [nodeName] = inspectCLI; | ||
}); | ||
return response; | ||
} | ||
}; | ||
}; |
@@ -10,8 +10,20 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Flag all the variables that were declared but never used' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var allVariableDeclarations = {}; | ||
//collect all variable declarations from VariableDeclarators and DeclarativeExpressions | ||
context.on ('VariableDeclarator', function (emitted) { | ||
function inspectVariableDeclarator (emitted) { | ||
var node = emitted.node; | ||
@@ -22,5 +34,5 @@ | ||
} | ||
}); | ||
} | ||
context.on ('DeclarativeExpression', function (emitted) { | ||
function inspectDeclarativeExpression (emitted) { | ||
var node = emitted.node; | ||
@@ -32,8 +44,8 @@ | ||
} | ||
}); | ||
} | ||
//While exiting Progam Node, all the vars that haven't been used still exist inside VariableDeclarations. Report them | ||
context.on ('Program', function (emitted) { | ||
function inspectProgram (emitted) { | ||
if (emitted.exit) { | ||
Object.keys (allVariableDeclarations).forEach (function (name) { | ||
@@ -45,8 +57,8 @@ context.report ({ | ||
}); | ||
} | ||
}); | ||
} | ||
//As soon as the first use of a variable is encountered, delete that variable's node from allVariableDeclarations | ||
context.on ('Identifier', function (emitted) { | ||
function inspectIdentifier (emitted) { | ||
if (!emitted.exit) { | ||
@@ -63,6 +75,14 @@ var node = emitted.node, | ||
} | ||
}); | ||
} | ||
return { | ||
Identifier: inspectIdentifier, | ||
Program: inspectProgram, | ||
DeclarativeExpression: inspectDeclarativeExpression, | ||
VariableDeclarator: inspectVariableDeclarator | ||
}; | ||
} | ||
}; | ||
}; |
@@ -10,5 +10,18 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
context.on ('WithStatement', function (emitted) { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure no use of with statements in the code' | ||
}, | ||
deprecated: true, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
function inspectWithStatement (emitted) { | ||
if (emitted.exit) { | ||
@@ -22,6 +35,10 @@ return; | ||
}); | ||
}); | ||
} | ||
return { | ||
WithStatement: inspectWithStatement | ||
}; | ||
} | ||
}; |
@@ -10,7 +10,67 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Ensure that operators are surrounded by a single space on either side' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var sourceCode = context.getSourceCode (); | ||
context.on ('BinaryExpression', function (emitted) { | ||
function inspectAssignmentExpression (emitted) { | ||
/** | ||
* node.operator is refined here by adding backslash before all the 'special' characters. | ||
* 'special' chars are thos chars that are part of solidity assignment operators and, if used without backslash in JS RegExp, | ||
* behave as wildcard characters. So to make sure they're treated as simple strings, we add '\' before them. | ||
* As of today, these chars include: * / + | ^ | ||
*/ | ||
var node = emitted.node, | ||
op = node.operator.replace (/([\+\*\/\|\^])/g, '\\$1'), opLength = node.operator.length; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
// If expression is 'abc *= def;', then charsAfterLeftNode will contain ' *= d'. | ||
var charsAfterLeftNode = sourceCode.getNextChars (node.left, 3 + opLength), | ||
validationRegexp = new RegExp ('^ ' + op + ' [^\\s]$'); | ||
(!validationRegexp.test (charsAfterLeftNode)) && context.report ({ | ||
node: node.left, | ||
message: 'Assignment operator must have exactly single space on both sides of it.' | ||
}); | ||
} | ||
//statement like `var x = 10` doesn't come under AssignmentExpression, so needs to be checked separately | ||
function inspectVariableDeclaration (emitted) { | ||
var node = emitted.node, code = sourceCode.getText (node); | ||
if (emitted.exit) { | ||
return; | ||
} | ||
//if a particular character is '=', check its left and right for single space | ||
for (var i = 2; i < code.length; i++) { | ||
if (code [i] === '=') { | ||
(!/^[^\/\s] $/.test (code.slice (i-2, i))) && context.report ({ | ||
node: node, | ||
message: 'There should be only a single space between assignment operator \'=\' and its left side.' | ||
}); | ||
(!/^ [^\/\s]$/.test (code.slice (i+1, i+3))) && context.report ({ | ||
node: node, | ||
message: 'There should be only a single space between assignment operator \'=\' and its right side.' | ||
}); | ||
} | ||
} | ||
} | ||
function inspectBinaryExpression (emitted) { | ||
var leftNode, | ||
@@ -124,6 +184,12 @@ node = emitted.node; | ||
}); | ||
} | ||
return { | ||
BinaryExpression: inspectBinaryExpression, | ||
VariableDeclaration: inspectVariableDeclaration, | ||
AssignmentExpression: inspectAssignmentExpression | ||
}; | ||
} | ||
}; | ||
}; |
@@ -10,8 +10,30 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
context.on ('Program', function (emitted) { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure a) A PRAGMA directive exists and b) its on top of the file' | ||
}, | ||
schema: [], | ||
fixable: 'code' | ||
}, | ||
create: function (context) { | ||
var missinNodeOnTopErrorReported = false; | ||
/** | ||
* This executes only when we're leaving the Program node. At that time, | ||
* we check whether the "missing pragma on top" error has already been reported or not. | ||
* If not, we proceed to report it here. This happens when there are no pragma statements in | ||
* the entire file. If there is one (but not on top of file), it gets reported by inspectPragmaStatement(). | ||
*/ | ||
function inspectProgram (emitted) { | ||
var node = emitted.node, body = node.body; | ||
if (emitted.exit) { | ||
if (!emitted.exit || missinNodeOnTopErrorReported) { | ||
return; | ||
@@ -24,10 +46,8 @@ } | ||
}); | ||
}); | ||
} | ||
context.on ('PragmaStatement', function (emitted) { | ||
function inspectPragmaStatement (emitted) { | ||
var node = emitted.node, | ||
sourceCode = context.getSourceCode (), | ||
pragmaParent = sourceCode.getParent (node), | ||
error; | ||
sourceCode = context.getSourceCode (), pragmaParent = sourceCode.getParent (node), error; | ||
@@ -38,2 +58,4 @@ if (emitted.exit) { | ||
var pragmaCode = sourceCode.getText (node); | ||
/** | ||
@@ -46,4 +68,3 @@ * Raise error if Pragma statement is not a direct child of the program | ||
node: node, | ||
message: 'Pragma Directive \"' + sourceCode.getText (node) + | ||
'\" cannot be inside ' + pragmaParent.type | ||
message: 'Pragma Directive "' + pragmaCode + '" cannot be inside ' + pragmaParent.type | ||
}; | ||
@@ -58,12 +79,21 @@ | ||
node: node, | ||
message: 'Pragma Directive \"' + sourceCode.getText (node) + | ||
'\" should only be at the top of the file.' | ||
message: 'Pragma Directive "' + pragmaCode + '" should only be at the top of the file.', | ||
fix: function (fixer) { | ||
return [fixer.remove (node), | ||
fixer.insertTextBefore (pragmaParent.body [0], pragmaCode + '\n')]; | ||
} | ||
}; | ||
missinNodeOnTopErrorReported = true; | ||
return context.report (error); | ||
} | ||
}); | ||
} | ||
return { | ||
Program: inspectProgram, | ||
PragmaStatement: inspectPragmaStatement | ||
}; | ||
} | ||
}; |
@@ -10,4 +10,16 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Ensure that all constants (and only constants) contain only upper case letters and underscore' | ||
}, | ||
schema: [] | ||
}, | ||
create: function (context) { | ||
var upperCaseRegEx = /^[A-Z][A-Z_0-9]*[A-Z0-9]$/; | ||
@@ -22,3 +34,3 @@ | ||
context.on ('StateVariableDeclaration', function (emitted) { | ||
function inspectStateVariableDeclaration (emitted) { | ||
var node = emitted.node; | ||
@@ -33,6 +45,10 @@ | ||
reportNode (node); | ||
}); | ||
} | ||
return { | ||
StateVariableDeclaration: inspectStateVariableDeclaration | ||
}; | ||
} | ||
}; | ||
}; |
@@ -10,9 +10,24 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
var disallowedNames = ['l', 'O', 'I']; | ||
docs: { | ||
recommended: true, | ||
type: 'error', | ||
description: 'Ensure that names "l", "O" & "I" are not used for variables' | ||
}, | ||
schema: [{ | ||
type: 'array', | ||
items: { type: 'string', minLength: 1 }, | ||
minItems: 1 | ||
}] | ||
}, | ||
create: function (context) { | ||
var disallowedNames = context.options ? context.options [0] : ['l', 'O', 'I']; | ||
function inspectVariableDeclarator (emitted) { | ||
var node = emitted.node, | ||
variableName = node.id.name; | ||
var node = emitted.node, variableName = node.id.name; | ||
@@ -27,3 +42,3 @@ if (emitted.exit) { | ||
node: node, | ||
message: 'Using \'' + variableName + '\' for a variable name should be avoided.' | ||
message: 'Using "' + variableName + '" for a variable name should be avoided.' | ||
}); | ||
@@ -35,4 +50,3 @@ } | ||
function inspectDeclarativeExpression (emitted) { | ||
var node = emitted.node, | ||
variableName = node.name; | ||
var node = emitted.node, variableName = node.name; | ||
@@ -53,7 +67,14 @@ if (emitted.exit) { | ||
context.on ('VariableDeclarator', inspectVariableDeclarator); | ||
context.on ('DeclarativeExpression', inspectDeclarativeExpression); | ||
// Aliased because both the inspect functions access the same property "name" of the node passed to them. | ||
// So no need for 2 separate functions for now (will create separate when different attrs are accessed in future) | ||
var inspectStateVariableDeclaration = inspectDeclarativeExpression; | ||
return { | ||
VariableDeclarator: inspectVariableDeclarator, | ||
DeclarativeExpression: inspectDeclarativeExpression, | ||
StateVariableDeclaration: inspectStateVariableDeclaration | ||
}; | ||
} | ||
}; | ||
}; |
@@ -10,79 +10,19 @@ /** | ||
verify: function (context) { | ||
meta: { | ||
var sourceCode = context.getSourceCode (); | ||
docs: { | ||
recommended: true, | ||
type: 'warning', | ||
description: 'Specify where whitespace is suitable and where it isn\'t' | ||
}, | ||
//same could potentially be applied to FunctionDeclaration | ||
context.on ('CallExpression', function (emitted) { | ||
var node = emitted.node, | ||
callArgs = node.arguments; | ||
schema: [] | ||
function inspectCallArgForCommaWhitespace (arg) { | ||
var charAfterArg = sourceCode.getNextChar (arg); | ||
}, | ||
(charAfterArg !== ',') && context.report ({ | ||
node: arg, | ||
location: { | ||
column: sourceCode.getEndingColumn (arg) + 1 | ||
}, | ||
message: 'There should be no whitespace or comments between argument and the comma following it.' | ||
}); | ||
} | ||
create: function (context) { | ||
if (emitted.exit) { | ||
return; | ||
} | ||
var sourceCode = context.getSourceCode (); | ||
var nodeCode = sourceCode.getText (node); | ||
//for a 0-argument call, ensure that name is followed by '()' | ||
if (!callArgs.length) { | ||
for (var i = nodeCode.length; i > 0; i--) { | ||
if (nodeCode [i] === ')' && nodeCode [i-1] === '(') { | ||
return; | ||
} | ||
if (/[\s\(\)]/.test (nodeCode [i])) { | ||
break; | ||
} | ||
} | ||
return context.report ({ | ||
node: node, | ||
message: '\"' + nodeCode + '\": ' + | ||
'A call without arguments should have brackets without any whitespace between them, like \'functionName ()\'.' | ||
}); | ||
} | ||
//CHECKING FOR COMMA WHITESPACE | ||
callArgs.slice (0, -1).forEach (inspectCallArgForCommaWhitespace); | ||
var lastCallArg = callArgs.slice (-1) [0]; | ||
//if call spans over multiple lines (due to too many arguments), below rules don't apply | ||
if (sourceCode.getLine (node) !== sourceCode.getEndingLine (lastCallArg)) { | ||
return; | ||
} | ||
var charBeforeFirstArg = sourceCode.getPrevChar (callArgs [0]), | ||
charAfterLastCallArg = sourceCode.getNextChar (lastCallArg); | ||
(callArgs [0].type !== 'NameValueAssignment' && charBeforeFirstArg !== '(') && context.report ({ | ||
node: callArgs [0], | ||
location: { | ||
column: sourceCode.getColumn (callArgs [0]) - 1 | ||
}, | ||
message: '\'' + node.callee.name + '\': The first argument must not be preceded by any whitespace or comments (only \'(\').' | ||
}); | ||
(lastCallArg.type !== 'NameValueAssignment' && charAfterLastCallArg !== ')') && context.report ({ | ||
node: callArgs [0], | ||
location: { | ||
column: sourceCode.getEndingColumn (lastCallArg) + 1 | ||
}, | ||
message: '\'' + node.callee.name + '\': The last argument must not be succeded by any whitespace or comments (only \')\').' | ||
}); | ||
}); | ||
context.on ('NameValueAssignment', function (emitted) { | ||
function inspectNameValueAssignment (emitted) { | ||
if (emitted.exit) { | ||
@@ -124,6 +64,6 @@ return; | ||
}); | ||
}); | ||
} | ||
context.on ('MemberExpression', function (emitted) { | ||
function inspectMemberExpression (emitted) { | ||
var node = emitted.node, | ||
@@ -162,6 +102,6 @@ property = node.property; | ||
}); | ||
}); | ||
} | ||
context.on ('BlockStatement', function (emitted) { | ||
function inspectBlockStatement (emitted) { | ||
var node = emitted.node, blockBody = node.body, | ||
@@ -208,20 +148,8 @@ lastBlockItem = blockBody.slice (-1) [0]; //if block is empty, this becomes undefined | ||
}); | ||
}); | ||
} | ||
context.on ('ObjectExpression', function (emitted) { | ||
function inspectObjectExpression (emitted) { | ||
var node = emitted.node, properties = node.properties; | ||
function inspectObjectPropForWhitespace (prop) { | ||
var charAfterProp = sourceCode.getNextChar (prop); | ||
(charAfterProp !== ',') && context.report ({ | ||
node: prop, | ||
location: { | ||
column: sourceCode.getEndingColumn (prop) + 1 | ||
}, | ||
message: 'There should be no whitespace or comments between object property and the comma following it.' | ||
}); | ||
} | ||
if (emitted.exit) { | ||
@@ -243,5 +171,2 @@ return; | ||
//CHECK FOR COMMA WHITESPACE | ||
properties.slice (0, -1).forEach (inspectObjectPropForWhitespace); | ||
var lastProperty = lastProperty = properties.slice (-1) [0]; | ||
@@ -272,330 +197,13 @@ | ||
}); | ||
}); | ||
} | ||
return { | ||
ObjectExpression: inspectObjectExpression, | ||
BlockStatement: inspectBlockStatement, | ||
MemberExpression: inspectMemberExpression, | ||
NameValueAssignment: inspectNameValueAssignment | ||
}; | ||
context.on ('AssignmentExpression', function (emitted) { | ||
/** | ||
* node.operator is refined here by adding backslash before all the 'special' characters. | ||
* 'special' chars are thos chars that are part of solidity assignment operators and, if used without backslash in JS RegExp, | ||
* behave as wildcard characters. So to make sure they're treated as simple strings, we add '\' before them. | ||
* As of today, these chars include: * / + | ^ | ||
*/ | ||
var node = emitted.node, | ||
op = node.operator.replace (/([\+\*\/\|\^])/g, '\\$1'), opLength = node.operator.length; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
// If expression is 'abc *= def;', then charsAfterLeftNode will contain ' *= d'. | ||
var charsAfterLeftNode = sourceCode.getNextChars (node.left, 3 + opLength), | ||
validationRegexp = new RegExp ('^ ' + op + ' [^\\s]$'); | ||
(!validationRegexp.test (charsAfterLeftNode)) && context.report ({ | ||
node: node.left, | ||
message: 'Assignment operator must have exactly single space on both sides of it.' | ||
}); | ||
}); | ||
//statement like `var x = 10` doesn't come under AssignmentExpression, so needs to be checked separately | ||
context.on ('VariableDeclaration', function (emitted) { | ||
var node = emitted.node, code = sourceCode.getText (node); | ||
if (emitted.exit) { | ||
return; | ||
} | ||
//if a particular character is '=', check its left and right for single space | ||
for (var i = 2; i < code.length; i++) { | ||
if (code [i] === '=') { | ||
(!/^[^\/\s] $/.test (code.slice (i-2, i))) && context.report ({ | ||
node: node, | ||
message: 'There should be only a single space between assignment operator \'=\' and its left side.' | ||
}); | ||
(!/^ [^\/\s]$/.test (code.slice (i+1, i+3))) && context.report ({ | ||
node: node, | ||
message: 'There should be only a single space between assignment operator \'=\' and its right side.' | ||
}); | ||
} | ||
} | ||
//ensure there's no whitespace or comments before semicolon | ||
(code [code.length - 1] === ';' && /(\s|\/)/.test (code [code.length - 2])) && context.report ({ | ||
node: node, | ||
location: { | ||
column: code.length - 2 | ||
}, | ||
message: 'There should be no whitespace or comments before the semicolon.' | ||
}); | ||
}); | ||
//If we're dealing with abstract function declaration, we need to ensure no whitespce or comments before semicolon | ||
context.on ('FunctionDeclaration', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
var code = sourceCode.getText (node); | ||
(node.is_abstract && code [code.length - 1] === ';' && /(\s|\/)/.test (code [code.length - 2])) && | ||
context.report ({ | ||
node: node, | ||
location: { | ||
column: code.length - 2 | ||
}, | ||
message: 'There should be no whitespace or comments before the semicolon.' | ||
}); | ||
//If parameters are specified, ensure appropriate spacing surrounding commas | ||
var params = node.params; | ||
if (params && params.length > 1) { | ||
params.slice (0, -1).forEach (function (arg) { | ||
sourceCode.getNextChar (arg) !== ',' && context.report ({ | ||
node: arg, | ||
location: { | ||
column: sourceCode.getEndingColumn (arg) + 1 | ||
}, | ||
message: node.name + ' ' + arg.id + '(): All arguments (except the last one) must be immediately followed by a comma.' | ||
}); | ||
}); | ||
} | ||
}); | ||
// The InformalParameter nodes (params) in modifier declaration should follow the same spacing rules as function declarations | ||
context.on ('ModifierDeclaration', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
//If parameters are specified, ensure appropriate spacing surrounding commas | ||
var params = node.params; | ||
if (params && params.length > 1) { | ||
params.slice (0, -1).forEach (function (arg) { | ||
sourceCode.getNextChar (arg) !== ',' && context.report ({ | ||
node: arg, | ||
location: { | ||
column: sourceCode.getEndingColumn (arg) + 1 | ||
}, | ||
message: node.name + ' ' + arg.id + '(): All arguments (except the last one) must be immediately followed by a comma.' | ||
}); | ||
}); | ||
} | ||
}); | ||
context.on ('IfStatement', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
/** | ||
* Ensure a single space between 'if' token and the opening parenthesis 'if (...)' | ||
*/ | ||
var ifTokenLength = 'if'.length, | ||
nodeCode = sourceCode.getText (node).slice (ifTokenLength, ifTokenLength + 2); | ||
(nodeCode !== ' (') && context.report ({ | ||
node: node, | ||
location: { | ||
column: sourceCode.getColumn (node) + ifTokenLength | ||
}, | ||
message: 'There should be exactly a single space between the \'if\' token and the parenthetic block representing the conditional.' | ||
}); | ||
}); | ||
context.on ('WhileStatement', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
/** | ||
* Ensure a single space between 'while' token and the opening parenthesis 'while (...)' | ||
*/ | ||
var whileTokenLength = 'while'.length, | ||
nodeCode = sourceCode.getText (node).slice (whileTokenLength, whileTokenLength + 2); | ||
(nodeCode !== ' (') && context.report ({ | ||
node: node, | ||
location: { | ||
column: sourceCode.getColumn (node) + whileTokenLength | ||
}, | ||
message: 'There should be exactly a single space between the \'while\' token and the parenthetic block representing the conditional.' | ||
}); | ||
}); | ||
context.on ('ForStatement', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
/** | ||
* Ensure a single space between 'for' token and the opening parenthesis 'for (...)' | ||
*/ | ||
var forTokenLength = 'for'.length, | ||
nodeCode = sourceCode.getText (node).slice (forTokenLength, forTokenLength + 2); | ||
(nodeCode !== ' (') && context.report ({ | ||
node: node, | ||
location: { | ||
column: sourceCode.getColumn (node) + forTokenLength | ||
}, | ||
message: 'There should be exactly a single space between the \'for\' token and the parenthetic block representing the conditional.' | ||
}); | ||
}); | ||
/************************************************************************************************ | ||
* From here on, we're explicitly looking for semicolon whitespace errors | ||
************************************************************************************************/ | ||
context.on ('ExpressionStatement', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
var code = sourceCode.getText (node); | ||
//ensure there's no whitespace or comments before semicolon | ||
(code [code.length - 1] === ';' && /(\s|\/)/.test (code [code.length - 2])) && context.report ({ | ||
node: node, | ||
location: { | ||
column: code.length - 2 | ||
}, | ||
message: 'There should be no whitespace or comments before the semicolon.' | ||
}); | ||
}); | ||
context.on ('UsingStatement', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
var code = sourceCode.getText (node); | ||
//ensure there's no whitespace or comments before semicolon | ||
(code [code.length - 1] === ';' && /(\s|\/)/.test (code [code.length - 2])) && context.report ({ | ||
node: node, | ||
location: { | ||
column: code.length - 2 | ||
}, | ||
message: 'There should be no whitespace or comments before the semicolon.' | ||
}); | ||
}); | ||
context.on ('ImportStatement', function (emitted) { | ||
var node = emitted.node; | ||
if (emitted.exit) { | ||
return; | ||
} | ||
var code = sourceCode.getText (node); | ||
//ensure there's no whitespace or comments before semicolon | ||
(code [code.length - 1] === ';' && /(\s|\/)/.test (code [code.length - 2])) && context.report ({ | ||
node: node, | ||
location: { | ||
column: code.length - 2 | ||
}, | ||
message: 'There should be no whitespace or comments before the semicolon.' | ||
}); | ||
}); | ||
/************************************************************************************************ | ||
* From here on, we're explicitly looking for comma whitespace errors | ||
************************************************************************************************/ | ||
context.on ('ArrayExpression', function (emitted) { | ||
var node = emitted.node; | ||
function inspectArrayElementForCommaWhitespace (elem) { | ||
var charAfterElem = sourceCode.getNextChar (elem); | ||
(charAfterElem !== ',') && context.report ({ | ||
node: elem, | ||
location: { | ||
column: sourceCode.getEndingColumn (elem) + 1 | ||
}, | ||
message: 'There should be no whitespace or comments between Array element and the comma following it.' | ||
}); | ||
} | ||
if (emitted.exit) { | ||
return; | ||
} | ||
//need to test comma from starting to second last elem, since there's no comma after last element | ||
node.elements.slice (0, -1).forEach (inspectArrayElementForCommaWhitespace); | ||
}); | ||
context.on ('SequenceExpression', function (emitted) { | ||
var node = emitted.node, expressions = node.expressions || []; | ||
function inspectExprForWhitespace (expr) { | ||
var charAfterExpr = sourceCode.getNextChar (expr); | ||
(charAfterExpr !== ',') && context.report ({ | ||
node: expr, | ||
location: { | ||
column: sourceCode.getEndingColumn (expr) + 1 | ||
}, | ||
message: 'There should be no whitespace or comments between Tuple\'s element and the comma following it.' | ||
}); | ||
} | ||
if (emitted.exit) { | ||
return; | ||
} | ||
expressions.slice (0, -1).forEach (inspectExprForWhitespace); | ||
}); | ||
context.on ('VariableDeclarationTuple', function (emitted) { | ||
var node = emitted.node, declarations = node.declarations; | ||
function inspectVariableDeclaratorForWhitespace (vd) { | ||
var charAfterExpr = sourceCode.getNextChar (vd); | ||
(charAfterExpr !== ',') && context.report ({ | ||
node: vd, | ||
location: { | ||
column: sourceCode.getEndingColumn (vd) + 1 | ||
}, | ||
message: '\'' + vd.id.name + '\': identifier should be immediately followed by a comma without any whitespace in between.' | ||
}); | ||
} | ||
if (emitted.exit) { | ||
return; | ||
} | ||
declarations.slice (0, -1).forEach (inspectVariableDeclaratorForWhitespace); | ||
}); | ||
} | ||
}; |
@@ -10,2 +10,3 @@ /** | ||
solExplore = require ('sol-explore'), | ||
util = require ('util'), | ||
@@ -22,4 +23,9 @@ EventEmitter = require ('events').EventEmitter, | ||
astUtils = require ('./utils/ast-utils'), | ||
jsUtils = require ('./utils/js-utils'); | ||
jsUtils = require ('./utils/js-utils'), | ||
configInspector = require ('./utils/config-inspector'), | ||
ruleInspector = require ('./utils/rule-inspector'), | ||
isErrObjectValid = require ('../config/schemas/error-supplied-to-solium').validationFunc, | ||
isValidFixerPacket = require ('../config/schemas/fixer-packet').validationFunc; | ||
module.exports = (function () { | ||
@@ -35,3 +41,3 @@ | ||
Solium.reset = function reset () { | ||
this.removeAllListeners (); | ||
Solium.removeAllListeners (); | ||
messages = []; | ||
@@ -49,5 +55,3 @@ sourceCodeText = ''; | ||
Solium.lint = function lint (sourceCode, config, noReset) { | ||
var nodeEventGenerator = new EventGenerator (Solium), | ||
AST = {}, | ||
errorObjects; | ||
var nodeEventGenerator = new EventGenerator (Solium), AST = {}, errorObjects; | ||
@@ -59,33 +63,84 @@ if (typeof sourceCode === 'object' && sourceCode.constructor.name === 'Buffer') { | ||
if (!(sourceCode && typeof sourceCode === 'string')) { | ||
throw new Error ( | ||
'A valid source code string was not passed.' | ||
); | ||
throw new Error ('A valid source code string was not provided.'); | ||
} | ||
if (!(config && typeof config === 'object' && config.rules && typeof config.rules === 'object')) { | ||
if (!configInspector.isValid (config)) { | ||
throw new Error ( | ||
'A valid configuration object was not passed. Please check the documentation.' | ||
'A valid configuration object was not passed.' + | ||
' Please see http://solium.readthedocs.io/en/latest/user-guide.html#configuring-the-linter' + | ||
' for a valid config format.' | ||
); | ||
} | ||
!noReset && this.reset (); | ||
!noReset && Solium.reset (); | ||
sourceCodeText = sourceCode; | ||
astUtils.init (sourceCodeText); | ||
currentConfig = JSON.parse (JSON.stringify (config)); //deep copy config object | ||
//load meta information of enabled rules | ||
currentConfig.rules = rules.load (currentConfig.rules, currentConfig ['custom-rules-filename']); | ||
currentConfig = JSON.parse (JSON.stringify (config)); // deep copy config object | ||
currentConfig.options = currentConfig.options || {}; // ensure "options" attr always exists in config | ||
//load meta information of rules | ||
if (configInspector.isFormatDeprecated (currentConfig)) { | ||
var crf = currentConfig ['custom-rules-filename']; | ||
Solium.reportInternal ({ | ||
type: 'warning', | ||
message: '[DEPRECATED] You are using a deprecated soliumrc configuration format. ' + | ||
'Please see http://solium.readthedocs.io/en/latest/user-guide.html#migrating-to-v1-0-0' + | ||
' to migrate from Solium v0 to v1.' | ||
}); | ||
crf && Solium.reportInternal ({ | ||
type: 'warning', | ||
message: '[DEPRECATED] Attribute "custom-rules-filename" is now deprecated. ' + | ||
'Rules from ' + crf + ' were not loaded. Plugins are supported v1 onward. Please see ' + | ||
'http://solium.readthedocs.io/en/latest/user-guide.html#custom-rule-injection-is-now-deprecated' | ||
}); | ||
currentConfig.rules = rules.loadUsingDeprecatedConfigFormat (currentConfig.rules, crf); | ||
} else { | ||
currentConfig.rules = rules.load (currentConfig); | ||
} | ||
Object.keys (currentConfig.rules).forEach (function (name) { | ||
var rule = rules.get (name); | ||
var rule = rules.get (name), currentRuleConfig = currentConfig.rules [name]; | ||
try { | ||
rule.verify ( | ||
new RuleContext (name, currentConfig.rules [name], rule.meta || {}, Solium) | ||
); | ||
} catch (e) { | ||
// Check for validity of exposed rule object | ||
if (!ruleInspector.isAValidRuleObject (rule)) { | ||
throw new Error ('A valid definition for rule "' | ||
+ name + '" was not provided. AJV message:\n' + util.inspect (ruleInspector.isAValidRuleObject.errors)); | ||
} | ||
// Check for validity of options passed to the rule via soliumrc (if options were passed) | ||
if (currentRuleConfig.options && !ruleInspector.areValidOptionsPassed (currentRuleConfig.options, rule.meta.schema)) { | ||
throw new Error ('Invalid options were passed to rule "' + name + '".'); | ||
} | ||
// If rule contains deprecated tag & is set to true, display deprecation notice. | ||
if (rule.meta.deprecated) { | ||
var message = '[DEPRECATED] Rule "' + name + '" is deprecated.'; | ||
if (rule.meta.docs.replacedBy) { | ||
message += ' Please use ' + rule.meta.docs.replacedBy.map (function (rn) { | ||
return '"' + rn + '"'; | ||
}).join (', ') + ' instead.'; | ||
} | ||
Solium.reportInternal ({ type: 'warning', message: message }); | ||
} | ||
// Call rule implementation's create() to retrieve the node names to listen for & their handlers | ||
// and subscribe them to the event emitter. | ||
var ruleNodeListeners = rule.create (new RuleContext (name, currentRuleConfig, rule.meta, Solium)); | ||
if (!ruleInspector.isAValidRuleResponseObject (ruleNodeListeners)) { | ||
throw new Error ( | ||
'A valid definition for rule \'' + name + '\' not found. Description:\n' + e.stack | ||
'A rule implementation\'s response must be an object whose keys are AST Nodes to listen for and values their corresponding handler functions. AJV message:\n' | ||
+ util.inspect (ruleInspector.isAValidRuleResponseObject.errors) | ||
); | ||
} | ||
Object.keys (ruleNodeListeners).forEach (function (node) { | ||
Solium.on (node, ruleNodeListeners [node]); | ||
}); | ||
}); | ||
@@ -96,5 +151,3 @@ | ||
} catch (e) { | ||
throw new Error ( | ||
'An error occured while parsing the source code:\n' + e | ||
); | ||
throw new Error ('An error occured while parsing the source code: ' + e.message); | ||
} | ||
@@ -119,2 +172,9 @@ | ||
// Remove all internal issues if user didn't ask for them. | ||
if (!currentConfig.options.returnInternalIssues) { | ||
messages = messages.filter (function (msg) { | ||
return !msg.internal; | ||
}); | ||
} | ||
//sort errors by line (column if line is same) | ||
@@ -142,3 +202,7 @@ messages.sort (function (a, b) { | ||
Solium.lintAndFix = function lintAndFix (sourceCode, config, noReset) { | ||
var errorObjects = this.lint (sourceCode, config); | ||
if (typeof sourceCode === 'object' && sourceCode.constructor.name === 'Buffer') { | ||
sourceCode = sourceCode.toString (); | ||
} | ||
var errorObjects = Solium.lint (sourceCode, config, noReset); | ||
var fixed = SourceCodeFixer.applyFixes (sourceCode, errorObjects); | ||
@@ -159,37 +223,41 @@ | ||
Solium.report = function report (error) { | ||
if (!jsUtils.isStrictlyObject (error)) { | ||
throw new Error ('Invalid error object'); | ||
if (!isErrObjectValid (error)) { | ||
throw new Error (util.inspect (error) + | ||
' is not a valid error object. AJV message:\n' + util.inspect (isErrObjectValid.errors)); | ||
} | ||
if (!astUtils.isASTNode (error.node)) { | ||
throw new Error ('Rule ' + error.ruleId + ' does not provide a valid AST node'); | ||
} | ||
error.location = error.location || {}; | ||
if (!(error.message && typeof error.message === 'string')) { | ||
throw new Error ( | ||
'Rule ' + error.ruleId + ' flags a node but doesn\'t provide an error description' | ||
); | ||
} | ||
if (!jsUtils.isStrictlyObject (error.location)) { | ||
error.location = {}; | ||
} | ||
var message = { | ||
ruleName: error.ruleName, | ||
type: error.type, //either 'error' or 'warning' | ||
type: error.type, // either 'error' or 'warning' | ||
node: error.node, | ||
message: error.message, | ||
line: error.location.line || astUtils.getLine (error.node), | ||
column: (error.location.column === 0) ? 0 : error.location.column || astUtils.getColumn (error.node) | ||
column: (error.location.column === 0) ? 0 : (error.location.column || astUtils.getColumn (error.node)) | ||
}; | ||
if (error.ruleMeta.fixable && error.fix) { | ||
if (typeof error.fix !== 'function') { | ||
throw new Error (error.ruleName + ': Attribute \'fix\' must be a function.'); | ||
// If rule supplies a fix, it can be added to the message reported after validation. | ||
if (error.fix) { | ||
if (!error.ruleMeta.fixable) { | ||
Solium.reportInternal ({ | ||
type: 'warning', message: '[WARNING] The fixes supplied by rule "' + | ||
error.ruleName + '" will be ignored since its "meta" doesn\'t contain the "fixable" property.' | ||
}); | ||
} else { | ||
if (typeof error.fix !== 'function') { | ||
throw new Error ('Rule "' + error.ruleName + | ||
'": Attribute "fix" (reported as part of the error "' + error.message + '") must be a function.'); | ||
} | ||
message.fix = error.fix (new RuleFixer (error.ruleMeta.fixable)); | ||
// Validate return value of the rule's error's fix() function | ||
if (!isValidFixerPacket (message.fix)) { | ||
throw new Error ('Rule "' + error.ruleName + | ||
'": the fix() method for rule error "' + error.message + '" returns an invalid value.'); | ||
} | ||
} | ||
message.fix = error.fix (new RuleFixer (error.ruleMeta.fixable)); | ||
} | ||
@@ -201,2 +269,15 @@ | ||
/** | ||
* Convenience wrapper for Solium modules to report internal issues. It adds the "internal: true" attr to error. | ||
* @param {Object} issue Internal issue | ||
*/ | ||
Solium.reportInternal = function reportInternal (issue) { | ||
if (!jsUtils.isStrictlyObject (issue)) { | ||
throw new Error ('Invalid error object'); | ||
} | ||
// Assign line & column = -1 so messages.sort() brings the internal issues on top | ||
messages.push (Object.assign (issue, { internal: true, line: -1, column: -1 })); | ||
}; | ||
/** | ||
* Provides the user with program source code wrapped inside a utility object that also provides functions to operate on the code | ||
@@ -203,0 +284,0 @@ * @returns {Object} sourceCodeObject The SourceCode Object that provides source text & functionality |
@@ -8,4 +8,17 @@ /** | ||
var sourceCodeText = ''; | ||
var Ajv = require ('ajv'), | ||
util = require ('util'), | ||
astNodeSchema = require ('../../config/schemas/ast-node'); | ||
var nodeSchemaValidator = new Ajv ({ allErrors: true }), sourceCodeText = ''; | ||
// For internal use. Throws if is passed an invalid AST node, else does nothing. | ||
function throwIfInvalidNode (node, functionName) { | ||
if (!exports.isASTNode (node)) { | ||
throw new Error (functionName + '(): ' + util.inspect (node) + ' is not a valid AST node.'); | ||
} | ||
} | ||
/** | ||
@@ -24,13 +37,4 @@ * Initialization method - provide all the necessary information astUtils functions could require in order to work | ||
*/ | ||
exports.isASTNode = function (possibleNode) { | ||
exports.isASTNode = nodeSchemaValidator.compile (astNodeSchema); | ||
return ( | ||
possibleNode !== null && //node shouldn't be null | ||
typeof possibleNode === 'object' && //must be data type object | ||
possibleNode.hasOwnProperty ('type') && //a 'type' key must exist in the node | ||
typeof possibleNode.type === 'string' //node.type's value must be a string | ||
); | ||
}; | ||
/** | ||
@@ -42,5 +46,3 @@ * Get the parent node of the specified node | ||
exports.getParent = function (node) { | ||
if (!exports.isASTNode (node)) { | ||
throw new Error (JSON.stringify (node) + ' is not a valid AST node'); | ||
} | ||
throwIfInvalidNode (node, 'getParent'); | ||
return node.parent; | ||
@@ -55,5 +57,3 @@ }; | ||
exports.getLine = function (node) { | ||
if (!exports.isASTNode (node)) { | ||
throw new Error (JSON.stringify (node) + ' is not a valid AST node'); | ||
} | ||
throwIfInvalidNode (node, 'getLine'); | ||
@@ -75,5 +75,3 @@ var newLineCharsBefore = sourceCodeText | ||
exports.getColumn = function (node) { | ||
if (!exports.isASTNode (node)) { | ||
throw new Error (JSON.stringify (node) + ' is not a valid AST node'); | ||
} | ||
throwIfInvalidNode (node, 'getColumn'); | ||
@@ -96,5 +94,3 @@ //start looking from sourceCodeText [node.start] and stop upon encountering the first linebreak character | ||
exports.getEndingLine = function (node) { | ||
if (!exports.isASTNode (node)) { | ||
throw new Error (JSON.stringify (node) + ' is not a valid AST node'); | ||
} | ||
throwIfInvalidNode (node, 'getEndingLine'); | ||
@@ -117,5 +113,3 @@ var newLineCharsBefore = sourceCodeText | ||
exports.getEndingColumn = function (node) { | ||
if (!exports.isASTNode (node)) { | ||
throw new Error (JSON.stringify (node) + ' is not a valid AST node'); | ||
} | ||
throwIfInvalidNode (node, 'getEndingColumn'); | ||
@@ -122,0 +116,0 @@ //start looking from 1 character before node.start and stop upon encountering the first linebreak character |
@@ -17,5 +17,5 @@ /** | ||
typeof possibleObject === 'object' && | ||
possibleObject.constructor !== Array | ||
possibleObject.constructor.name === 'Object' | ||
); | ||
}, | ||
}; |
{ | ||
"name": "solium", | ||
"version": "0.5.5", | ||
"description": "A flexible, stand-alone linter for Ethereum Solidity", | ||
"version": "1.0.0", | ||
"description": "A customisable linter to identify & fix patterns in Ethereum Solidity", | ||
"main": "./lib/solium.js", | ||
@@ -14,5 +14,13 @@ "bin": { | ||
"keywords": [ | ||
"Lint", | ||
"Solidity", | ||
"Abstract-Syntax-Tree" | ||
"lint", | ||
"static-analysis", | ||
"solidity", | ||
"abstract-syntax-tree", | ||
"ethereum", | ||
"smart-contracts", | ||
"solium", | ||
"blockchain", | ||
"code-quality", | ||
"dapp", | ||
"developer-tools" | ||
], | ||
@@ -26,9 +34,11 @@ "repository": { | ||
"dependencies": { | ||
"ajv": "^5.2.2", | ||
"chokidar": "^1.6.0", | ||
"colors": "^1.1.2", | ||
"commander": "^2.9.0", | ||
"js-string-escape": "^1.0.1", | ||
"lodash": "^4.14.2", | ||
"sol-digger": "0.0.2", | ||
"sol-explore": "^1.6.1", | ||
"solparse": "^1.2.2" | ||
"solparse": "^1.2.7" | ||
}, | ||
@@ -40,4 +50,8 @@ "devDependencies": { | ||
"should": "^11.0.0", | ||
"supertest": "^2.0.0" | ||
"solium-config-test": "0.0.1", | ||
"solium-config-test-invalid-schema": "0.0.0", | ||
"solium-config-test-invalid-syntax": "0.0.0", | ||
"solium-plugin-test": "^0.1.5", | ||
"solium-plugin-test-invalid-schema": "0.0.0" | ||
} | ||
} |
@@ -0,3 +1,14 @@ | ||
# Solium v1 is now in Beta! | ||
v1 comes packed with automatic code formatting, sharable configs, plugin system, bug fixes, pretty printing and a lot more! | ||
To install v1, run `npm install -g solium@v1` and access the complete documentation [here](http://solium.readthedocs.io/). | ||
If you'd like to use the `latest` (stable) version instead, please see below. | ||
___ | ||
![solium](https://cloud.githubusercontent.com/assets/12758282/18283522/4b206522-7483-11e6-9bcd-2a70ebc8cfdb.png) | ||
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/Solium-linter/Lobby) | ||
[![Build Status](https://travis-ci.org/duaraghav8/Solium.svg?branch=master)](https://travis-ci.org/duaraghav8/Solium) | ||
[![Latest News](https://img.shields.io/badge/Blog-Medium-yellowgreen.svg)](https://medium.com/solium) | ||
Solium is a linter for Solidity which uses Abstract Syntax Trees and allows the user to enable/disable existing rules and add their own ones! | ||
@@ -40,8 +51,6 @@ | ||
2. When new rules are added in subsequent versions and you update Solium, you need not re-initialize with ```--init```. Simply run ```solium --sync``` in your root directory and it automatically adds the newly added rules to your ```.soliumrc.json```. The sync option **doesn't change anything else in your configuration files**. | ||
2. Use `solium --reporter=gcc` or `solium --reporter=pretty` to configure the output | ||
3. Use ```solium --dir <DIRECTORY_NAME>``` to run the linter over a particular directory | ||
4. Use `solium --reporter=gcc` or `solium --reporter=pretty` to configure the output | ||
# Plugging in your custom rules | ||
@@ -129,5 +138,2 @@ -> Open up the ```.soliumrc.json``` configuration file and set the value of ```custom-rules-filename``` to the path of the file that defines your rules. You can either provide an absolute path or a path relative to the directory in which .soliumrc.json resides. For example: ```"custom-rules-filename": "./my-rules.js"``` | ||
# Contributing | ||
Please see the [Developer Guide](https://github.com/duaraghav8/Solium/blob/master/docs/DEVELOPER.md) to understand how to contribute rules to this repository. | ||
## Setup | ||
@@ -158,3 +164,5 @@ | ||
If you're using vim with syntastic, and prefer to use a locally installed version of Solium (rather than a global version), you can install [syntastic-local-solium](https://github.com/sohkai/syntastic-local-solium.vim) to automatically load the local version in packages that have installed their own. | ||
# License | ||
## MIT |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1
166
261302
9
9
70
4478
12
1
+ Addedajv@^5.2.2
+ Addedjs-string-escape@^1.0.1
+ Addedajv@5.5.2(transitive)
+ Addedco@4.6.0(transitive)
+ Addedfast-deep-equal@1.1.0(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedjs-string-escape@1.0.1(transitive)
+ Addedjson-schema-traverse@0.3.1(transitive)
Updatedsolparse@^1.2.7