diff --git a/CHANGELOG.md b/CHANGELOG.md index b508e9818..669cc26ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +## Version 1.7.3 + * Parsing: Use the harmony parser via the esnext flag in the config (Joel Kemp) + * validateIndentation: handle breakless case statements (Mike Sherov) + +## Version 1.7.2 + * validateIndentation: fix return in switch failure (Mike Sherov) + * Cast StringChecker maxErrors property to Number the first time (Florian Fesseler) + * Fix format of --esnext and --max-errors in README (Joe Lencioni) + +## Version 1.7.1 + * validateIndentation: fix empty multiline function body regression (Mike Sherov) + +## Version 1.7.0 + * validateJSDoc: Deprecate rule (Joel Kemp) + * Updated google preset (Richard Poole) + * Add "requireSpaceBeforeBlockStatements" rule into the jquery preset (Oleg Gaidarenko) + + * CLI: Support --esnext to Parse ES6. (Robert Jackson) + * CLI: Support a --max-errors option to limit the number of reported errors (mdevils) + + * New Rules: (require|disallow)CapitalizedComments (Joel Kemp) + * New Rules: (require|disallow)SpacesInCallExpression (Mathieu Schroeter) + * New Rules: (disallow|require)FunctionDeclarations (Nikhil Benesch) + * New Rules: (require|disallow)PaddingNewLinesInObjects (Valentin Agachi) + + * Implement "only" for parens rule (Oleg Gaidarenko) + * Simplify "allButNested" option for spaces rule (Oleg Gaidarenko) + * Implement "except" option for spaces rule (Oleg Gaidarenko) + * disallowMultipleVarDecl: Strict mode to disallow for statement exception (Joel Kemp) + + * disallowSpaceBeforeObjectKeys: fix parenthesised property value (Jindrich Besta) + * requireSpaceBeforeObjectValues: fix parenthesised property value (Jindrich Besta) + * validateIndentation: Allow non-indented "break" in "switch" statement (kevin.destrem) + * ValidateIndentation: remove array and object indentation validation (Mike Sherov) + * validateIndentation: Allow the "if" test to be nested. (Jesper Birkestrøm) + * ValidateIndentation: Relax indentation rules for function expressions. (Mike Sherov) + * requireCurlyBraces: support the with statement (Joel Kemp) + * Fix invalid result of findXxxxToken methods when value is provided (Romain Guerin) + * requireSpaceAfterLineComment: skips msjsdoc comments (Alexej Yaroshevich) + + * Docs: add a table of contents to README (Henry Zhu) + * Docs: Make version numbers real markdown headers (Alexander Artemenko) + ## Version 1.6.2 * Fix disallowMultipleLineBreaks with shebang line (Nicolas Gallagher) * Improve validateParameterSeparator rule (David Chambers) diff --git a/README.md b/README.md index d00c7cad3..916068145 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,21 @@ JSCS — JavaScript Code Style. **This is a documentation for the development version, please refer to the https://www.npmjs.org/package/jscs instead** +## Table of Contents + +- [Presets](#presets) +- [Friendly Packages](#friendly-packages) +- [Extensions](#extensions) +- [Installation](#installation) +- [CLI](#cli) +- [Options](#options) +- [Error Suppression](#error-suppression) +- [Versioning & Semver](#versioning--semver) +- [Rules](#rules) +- [Removed Rules](#removed-rules) +- [Browser Usage](#browser-usage) +- [How to Contribute](https://github.com/jscs-dev/node-jscs/blob/master/CONTRIBUTING.md) + ## Presets * [Airbnb](presets/airbnb.json) - https://github.com/airbnb/javascript @@ -63,7 +78,7 @@ Allows to define path to the config file. jscs path[ path[...]] --config=./.config.json ``` -If there is no `--config` option specified, `jscs` it will consequentially search for `jscsConfig` option in `package.json` file then for `.jscsrc` and `.jscs.json` files in the current working directory then in nearest ancestor until it hits the system root. +If there is no `--config` option specified, `jscs` it will consequentially search for `jscsConfig` option in `package.json` file then for `.jscsrc` (which is a just JSON with comments) and `.jscs.json` files in the current working directory then in nearest ancestor until it hits the system root. ### `--preset` If defined will use predefined rules for specific code style. @@ -82,10 +97,13 @@ But you also can specify your own reporter, since this flag accepts relative or jscs path[ path[...]] --reporter=./some-dir/my-reporter.js ``` +### `--esnext` +Attempts to parse your code as ES6 using the harmony version of the esprima parser. Please note that this is currently experimental, and will improve over time. + ### `--no-colors` Clean output without colors. -### '--max-errors' +### `--max-errors` Set the maximum number of errors to report ### `--help` @@ -183,6 +201,20 @@ Default: Infinity "maxErrors": 10 ``` +### esnext + +Attempts to parse your code as ES6 using the harmony version of the esprima parser. + +Type: `Boolean` + +Value: `true` + +#### Example + +```js +"esnext": true +``` + ## Error Suppression ### Inline Comments @@ -264,20 +296,6 @@ Type: `Array` or `Boolean` Values: Array of quoted keywords or `true` to require curly braces after the following keywords: -```js -[ - 'if', - 'else', - 'for', - 'while', - 'do', - 'try', - 'catch', - 'case', - 'default' -] -``` - JSHint: [`curly`](http://jshint.com/docs/options/#curly) #### Example @@ -310,6 +328,40 @@ if (x) { if (x) x++; ``` +### requireSpaceBeforeKeywords + +Requires space before keyword. + +Type: `Array` or `Boolean` + +Values: Array of quoted keywords or `true` to require all possible keywords to have a preceding space. + +#### Example + +```js +"requireSpaceBeforeKeywords": [ + "else", + "while", + "catch" +] +``` + +##### Valid + +```js +} else { + x++; +} +``` + +##### Invalid + +```js +}else { + x++; +} +``` + ### requireSpaceAfterKeywords Requires space after keyword. @@ -385,6 +437,39 @@ if(x > y) { ``` +### disallowSpaceBeforeKeywords + +Disallows space before keyword. + +Type: `Array` or `Boolean` + +Values: Array of quoted keywords or `true` to disallow spaces before all possible keywords. + +#### Example + +```js +"disallowSpaceBeforeKeywords": [ + "else", + "catch" +] +``` + +##### Valid + +```js +}else { + y--; +} +``` + +##### Invalid + +```js +} else { + y--; +} +``` + ### requireSpaceBeforeBlockStatements Requires space before block statements (for loops, control structures). @@ -403,15 +488,15 @@ Values: `true` ```js if (cond) { - foo(); + foo(); } for (var e in elements) { - bar(e); + bar(e); } while (cond) { - foo(); + foo(); } ``` @@ -419,15 +504,15 @@ while (cond) { ```js if (cond){ - foo(); + foo(); } for (var e in elements){ - bar(e); + bar(e); } while (cond){ - foo(); + foo(); } ``` @@ -450,15 +535,15 @@ Values: `true` ```js if (cond){ - foo(); + foo(); } for (var e in elements){ - bar(e); + bar(e); } while (cond){ - foo(); + foo(); } ``` @@ -466,15 +551,15 @@ while (cond){ ```js if (cond) { - foo(); + foo(); } for (var e in elements) { - bar(e); + bar(e); } while (cond) { - foo(); + foo(); } ``` @@ -590,7 +675,7 @@ var a = b?c: d; ### requireSpacesInFunctionExpression -Requires space before `()` or `{}` in function expressions (both named and anonymous). +Requires space before `()` or `{}` in function expressions (both [named](#requirespacesinnamedfunctionexpression) and [anonymous](#requirespacesinanonymousfunctionexpression)). Type: `Object` @@ -608,21 +693,21 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ##### Valid ```js -function () {} -function a () {} +var x = function () {}; +var x = function a () {}; ``` ##### Invalid ```js -function() {} -function (){} +var x = function() {}; +var x = function a(){}; ``` ### disallowSpacesInFunctionExpression -Disallows space before `()` or `{}` in function expressions (both named and anonymous). +Disallows space before `()` or `{}` in function expressions (both [named](#disallowspacesinnamedfunctionexpression) and [anonymous](#disallowspacesinanonymousfunctionexpression)). Type: `Object` @@ -640,15 +725,15 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ##### Valid ```js -function(){} -function a(){} +var x = function(){}; +var x = function a(){}; ``` ##### Invalid ```js -function () {} -function a (){} +var x = function () {}; +var x = function a (){}; ``` @@ -672,14 +757,21 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ##### Valid ```js -function () {} +var foo = function () {}; +var Foo = { + foo: function () {}; +} +array.map(function () {}); ``` ##### Invalid ```js -function() {} -function (){} +var foo = function() {}; +var Foo = { + foo: function (){}; +} +array.map(function(){}); ``` @@ -703,14 +795,21 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ##### Valid ```js -function(){} +var foo = function(){}; +var Foo = { + foo: function(){}; +} +array.map(function(){}); ``` ##### Invalid ```js -function () {} -function (){} +var foo = function () {}; +var Foo = { + foo: function (){}; +} +array.map(function() {}); ``` @@ -734,14 +833,14 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ##### Valid ```js -var x = function a () {} +var x = function a () {}; ``` ##### Invalid ```js -var x = function a() {} -var x = function a(){} +var x = function a() {}; +var x = function a(){}; ``` @@ -765,14 +864,14 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ##### Valid ```js -function a(){} +var x = function a(){}; ``` ##### Invalid ```js -function a () {} -function a (){} +var x = function a () {}; +var x = function a (){}; ``` @@ -840,7 +939,7 @@ function a (){} ### requireSpacesInFunction -Requires space before `()` or `{}` in function declarations and expressions. +Requires space before `()` or `{}` in function [declarations](#requirespacesinfunctiondeclaration) and [expressions](#requirespacesinfunctionexpression). Type: `Object` @@ -859,6 +958,8 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ```js function a () {} + +var x = function a () {}; ``` ##### Invalid @@ -866,12 +967,15 @@ function a () {} ```js function a() {} function a (){} + +var x = function a() {}; +var x = function a () {}; ``` ### disallowSpacesInFunction -Disallows space before `()` or `{}` in function declarations and expressions. +Disallows space before `()` or `{}` in function [declarations](#disallowspacesinfunctiondeclaration) and [expressions](#disallowspacesinfunctionexpression). Type: `Object` @@ -890,6 +994,8 @@ Values: `"beforeOpeningRoundBrace"` and `"beforeOpeningCurlyBrace"` as child pro ```js function a(){} + +var x = function a(){}; ``` ##### Invalid @@ -897,6 +1003,9 @@ function a(){} ```js function a () {} function a (){} + +var x = function a () {}; +var x = function a (){}; ``` @@ -1225,6 +1334,134 @@ foo({ }); ``` +### requirePaddingNewlinesBeforeKeywords + +Requires an empty line above the specified keywords unless the keyword is the first expression in a block. + +Type: `Array` or `Boolean` + +Values: Array of quoted types or `true` to require padding new lines after all of the keywords below. + +#### Example + +```js +"requirePaddingNewlinesBeforeKeywords": [ + "do", + "for", + "if", + "else", + "switch", + "case", + "try", + "catch", + "void", + "while", + "with", + "return", + "typeof", + "function" +] +``` + +##### Valid + +```js +function(a) { + if (!a) { + return false; + } + + for (var i = 0; i < b; i++) { + if (!a[i]) { + return false; + } + } + + return true; +} +``` + +##### Invalid + +```js +function(a) { + if (!a) { + return false; + } + for (var i = 0; i < b; i++) { + if (!a[i]) { + return false; + } + } + return true; +} +``` + +### disallowPaddingNewlinesBeforeKeywords + +Disallow an empty line above the specified keywords. + +Type: `Array` or `Boolean` + +Values: Array of quoted types or `true` to disallow padding new lines after all of the keywords below. + +#### Example + +```js +"requirePaddingNewlinesBeforeKeywords": [ + "do", + "for", + "if", + "else", + "switch", + "case", + "try", + "catch", + "void", + "while", + "with", + "return", + "typeof", + "function" +] +``` + +##### Valid + +```js +function(a) { + if (!a) { + return false; + } + for (var i = 0; i < b; i++) { + if (!a[i]) { + return false; + } + } + return true; +} +``` + +##### Invalid + +```js +function(a) { + if (!a) { + + return false; + } + + for (var i = 0; i < b; i++) { + if (!a[i]) { + + return false; + } + } + + return true; +} +``` + ### disallowEmptyBlocks Disallows empty blocks (except for catch blocks). @@ -1498,32 +1735,39 @@ var x = {'a': 1}; ### disallowDanglingUnderscores -Disallows identifiers that start or end in `_`, except for some popular exceptions: +Disallows identifiers that start or end in `_`. Some popular identifiers are automatically listed as exceptions: + - `__proto__` (javascript) - `_` (underscore.js) - `__filename` (node.js global) - `__dirname` (node.js global) + - `super_` (node.js, used by [`util.inherits`](http://nodejs.org/docs/latest/api/util.html#util_util_inherits_constructor_superconstructor)) -Type: `Boolean` +Type: `Boolean` or `Object` -Values: `true` +Values: + - `true` + - `Object`: + - `allExcept`: array of quoted identifiers JSHint: [`nomen`](http://www.jshint.com/docs/options/#nomen) #### Example ```js -"disallowDanglingUnderscores": true +"disallowDanglingUnderscores": { allExcept: ["_exception"] } ``` ##### Valid ```js var x = 1; +var o = obj.__proto__; var y = _.extend; var z = __dirname; var w = __filename; var x_y = 1; +var v = _exception; ``` ##### Invalid @@ -2544,7 +2788,7 @@ Values: `true` ##### Valid ```js if (1 == a) { - return + return } ``` @@ -2552,7 +2796,7 @@ if (1 == a) { ```js if (a == 1) { - return + return } ``` @@ -2574,7 +2818,7 @@ Values: `true` ```js if (a == 1) { - return + return } ``` @@ -2582,22 +2826,26 @@ if (a == 1) { ```js if (1 == a) { - return + return } ``` ### requireSpaceAfterLineComment -Requires that a line comment (`//`) be followed by a space or slash space (`/// `). +Requires that a line comment (`//`) be followed by a space. -Type: `Boolean` or `String` +Type: `Boolean` or `Object` or `String` -Values: `true` or `'allowSlash'` +Values: + - `true` + - `"allowSlash"` (*deprecated* use `"except": ["/"]`) allows `/// ` format + - `Object`: + - `allExcept`: array of allowed strings before space `//(here) ` #### Example ```js -"requireSpaceAfterLineComment": true +"requireSpaceAfterLineComment": { "allExcept": ["#", "="] } ``` ##### Valid @@ -2605,6 +2853,8 @@ Values: `true` or `'allowSlash'` ```js // A comment /*A comment*/ +//# sourceURL=filename.js +//= require something ``` ##### Invalid @@ -2701,7 +2951,7 @@ var a = function(){ $('#foo').click(function(){ -};) +}) ``` ##### Invalid @@ -2713,7 +2963,7 @@ var a = function foo(){ $('#foo').click(function bar(){ -};) +}); ``` ### disallowFunctionDeclarations diff --git a/bin/jscs b/bin/jscs index 57ffd1076..71e490b7f 100755 --- a/bin/jscs +++ b/bin/jscs @@ -14,11 +14,12 @@ program .version(require('../package.json').version) .usage('[options] ') .option('-c, --config [path]', 'configuration file path') + .option('-e, --esnext', 'attempts to parse esnext code (currently es6)') .option('-n, --no-colors', 'clean output without colors') .option('-p, --preset ', 'preset config') - .option('-r, --reporter ', 'error reporter, console - default, text, checkstyle, junit, inline') .option('-v, --verbose', 'adds rule names to the error output') .option('-m, --max-errors ', 'maximum number of errors to report') + .option('-r, --reporter ', 'error reporter, console - default, text, checkstyle, junit, inline') .option('', 'Also accepts relative or absolute path to custom reporter') .option('', 'For instance:') .option('', '\t ../some-dir/my-reporter.js\t(relative path with extension)') diff --git a/lib/checker.js b/lib/checker.js index b32f38ca2..b366fcac2 100644 --- a/lib/checker.js +++ b/lib/checker.js @@ -4,9 +4,7 @@ var StringChecker = require('./string-checker'); var utils = require('util'); var path = require('path'); -var additionalRules = require('./options/additional-rules'); -var excludeFiles = require('./options/exclude-files'); -var fileExtensions = require('./options/file-extensions'); +var NodeConfiguration = require('./config/node-configuration'); /** * Starts Code Style checking process. @@ -25,13 +23,9 @@ utils.inherits(Checker, StringChecker); * @param {Object} config */ Checker.prototype.configure = function(config) { - var cwd = config.configPath ? path.dirname(config.configPath) : process.cwd(); - - fileExtensions(config, this); - excludeFiles(config, this, cwd); - additionalRules(config, this, cwd); - StringChecker.prototype.configure.apply(this, arguments); + + this._fileExtensions = this._configuration.getFileExtensions(); }; /** @@ -41,18 +35,13 @@ Checker.prototype.configure = function(config) { * @returns {Promise * Errors} */ Checker.prototype.checkFile = function(path) { - var _this = this; - - if (!_this._isExcluded(path)) { + if (!this._configuration.isFileExcluded(path)) { return vowFs.read(path, 'utf8').then(function(data) { - return _this.checkString(data, path); - }); + return this.checkString(data, path); + }, this); } - var defer = Vow.defer(); - defer.resolve(null); - - return defer.promise(); + return Vow.resolve(null); }; /** @@ -62,38 +51,35 @@ Checker.prototype.checkFile = function(path) { * @returns {Promise * Error[]} */ Checker.prototype.checkDirectory = function(path) { - var _this = this; - return vowFs.listDir(path).then(function(filenames) { return Vow.all(filenames.map(function(filename) { var fullname = path + '/' + filename; - // check for exclude path - if (_this._isExcluded(fullname)) { + if (this._configuration.isFileExcluded(fullname)) { return []; } return vowFs.stat(fullname).then(function(stat) { if (stat.isDirectory()) { - return _this.checkDirectory(fullname); + return this.checkDirectory(fullname); } - if (!_this._hasCorrectExtension(fullname)) { + if (!this._hasCorrectExtension(fullname)) { return []; } - return Vow.when(_this.checkFile(fullname)).then(function(errors) { + return Vow.when(this.checkFile(fullname)).then(function(errors) { if (errors) { return errors; } return []; }); - }); - })).then(function(results) { + }, this); + }, this)).then(function(results) { return [].concat.apply([], results); }); - }); + }, this); }; /** @@ -152,19 +138,6 @@ Checker.prototype.checkStdin = function() { return deferred.promise(); }; -/** - * Returns true if specified path is in excluded list. - * - * @returns {Boolean} - */ -Checker.prototype._isExcluded = function(testPath) { - testPath = path.resolve(testPath); - - return !this._excludes.every(function(exclude) { - return !exclude.match(testPath); - }); -}; - /** * Returns true if the file extension matches a file extension to process. * @@ -181,4 +154,14 @@ Checker.prototype._hasCorrectExtension = function(testPath) { ); }; +/** + * Returns new configuration instance. + * + * @protected + * @returns {Configuration} + */ +Checker.prototype._createConfiguration = function() { + return new NodeConfiguration(); +}; + module.exports = Checker; diff --git a/lib/cli.js b/lib/cli.js index e3821c4ce..3e4575f9c 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -7,7 +7,6 @@ */ var Checker = require('./checker'); var configFile = require('./cli-config'); -var preset = require('./options/preset'); var Vow = require('vow'); var supportsColor = require('supports-color'); @@ -23,7 +22,10 @@ module.exports = function(program) { var config; var defer = Vow.defer(); var promise = defer.promise(); - var checker = new Checker(program.verbose); + var checker = new Checker({ + verbose: program.verbose, + esnext: program.esnext + }); var args = program.args; var returnArgs = { checker: checker, @@ -60,8 +62,8 @@ module.exports = function(program) { return returnArgs; } - if (program.preset && !preset.exists(program.preset)) { - console.error(preset.getDoesNotExistError(program.preset)); + if (program.preset && !checker.getConfiguration().hasPreset(program.preset)) { + console.error('Preset "%s" does not exist', program.preset); defer.reject(1); return returnArgs; @@ -79,11 +81,11 @@ module.exports = function(program) { } if (program.preset) { - config.preset = program.preset; + checker.getConfiguration().override({preset: program.preset}); } if (program.maxErrors) { - config.maxErrors = Number(program.maxErrors); + checker.getConfiguration().override({maxErrors: Number(program.maxErrors)}); } if (program.reporter) { @@ -110,7 +112,7 @@ module.exports = function(program) { return returnArgs; } - checker.registerDefaultRules(); + checker.getConfiguration().registerDefaultRules(); checker.configure(config); // Handle usage like 'cat myfile.js | jscs' or 'jscs -'' diff --git a/lib/config/configuration.js b/lib/config/configuration.js new file mode 100644 index 000000000..eb676edd3 --- /dev/null +++ b/lib/config/configuration.js @@ -0,0 +1,530 @@ +var assert = require('assert'); +var path = require('path'); +var minimatch = require('minimatch'); + +var BUILTIN_OPTIONS = { + plugins: true, + preset: true, + excludeFiles: true, + additionalRules: true, + fileExtensions: true, + maxErrors: true, + configPath: true, + esnext: true +}; + +/** + * JSCS Configuration. + * Browser/Rhino-compatible. + * + * @name Configuration + */ +function Configuration() { + this._presets = {}; + this._rules = {}; + this._configuredRules = []; + this._fileExtensions = ['.js']; + this._excludedFileMasks = []; + this._excludedFileMatchers = []; + this._ruleSettings = {}; + this._maxErrors = null; + this._basePath = '.'; + this._overrides = {}; + this._presetName = null; + this._esnextEnabled = false; +} + +/** + * Load settings from a configuration. + * + * @param {Object} config + */ +Configuration.prototype.load = function(config) { + this._throwNonCamelCaseErrorIfNeeded(config); + + var overrides = this._overrides; + var currentConfig = {}; + Object.keys(config).forEach(function(key) { + currentConfig[key] = config[key]; + }); + Object.keys(overrides).forEach(function(key) { + currentConfig[key] = overrides[key]; + }); + + var ruleSettings = this._processConfig(currentConfig); + var processedSettings = {}; + var unsupportedRules = []; + Object.keys(ruleSettings).forEach(function(optionName) { + var rule = this._rules[optionName]; + if (rule) { + var optionValue = ruleSettings[optionName]; + if (optionValue !== null) { + rule.configure(ruleSettings[optionName]); + this._configuredRules.push(rule); + processedSettings[optionName] = ruleSettings[optionName]; + } + } else { + unsupportedRules.push(optionName); + } + }, this); + if (unsupportedRules.length > 0) { + throw new Error('Unsupported rules: ' + unsupportedRules.join(', ')); + } + this._ruleSettings = processedSettings; +}; + +Configuration.prototype.getProcessedConfig = function() { + var result = {}; + Object.keys(this._ruleSettings).forEach(function(key) { + result[key] = this._ruleSettings[key]; + }, this); + result.excludeFiles = this._excludedFileMasks; + result.fileExtensions = this._fileExtensions; + result.maxErrors = this._maxErrors; + result.preset = this._presetName; + return result; +}; + +/** + * Returns list of configured rules. + * + * @returns {Rule[]} + */ +Configuration.prototype.getConfiguredRules = function() { + return this._configuredRules; +}; + +/** + * Returns excluded file mask list. + * + * @returns {String[]} + */ +Configuration.prototype.getExcludedFileMasks = function() { + return this._excludedFileMasks; +}; + +/** + * Returns `true` if specified file path is excluded. + * + * @param {String} filePath + * @returns {Boolean} + */ +Configuration.prototype.isFileExcluded = function(filePath) { + filePath = path.resolve(filePath); + return this._excludedFileMatchers.some(function(matcher) { + return matcher.match(filePath); + }); +}; + +/** + * Returns file extension list. + * + * @returns {String[]} + */ +Configuration.prototype.getFileExtensions = function() { + return this._fileExtensions; +}; + +/** + * Returns maximal error count. + * + * @returns {Number|undefined} + */ +Configuration.prototype.getMaxErrors = function() { + return this._maxErrors; +}; + +/** + * Returns `true` if `esnext` option is enabled. + * + * @returns {Boolean} + */ +Configuration.prototype.isESNextEnabled = function() { + return this._esnextEnabled; +}; + +/** + * Returns base path. + * + * @returns {String} + */ +Configuration.prototype.getBasePath = function() { + return this._basePath; +}; + +/** + * Overrides specified settings. + * + * @param {String} overrides + */ +Configuration.prototype.override = function(overrides) { + Object.keys(overrides).forEach(function(key) { + this._overrides[key] = overrides[key]; + }, this); +}; + +/** + * Processes configuration and returns config options. + * + * @param {Object} config + * @returns {Object} + */ +Configuration.prototype._processConfig = function(config) { + var ruleSettings = {}; + + // Base path + if (config.configPath) { + assert( + typeof config.configPath === 'string', + '`configPath` option requires string value' + ); + this._basePath = path.dirname(config.configPath); + } + + // Load plugins + if (config.plugins) { + assert(Array.isArray(config.plugins), '`plugins` option requires array value'); + config.plugins.forEach(this._loadPlugin, this); + } + + // Apply presets + var presetName = config.preset; + if (presetName) { + this._presetName = presetName; + assert(typeof presetName === 'string', '`preset` option requires string value'); + var presetData = this._presets[presetName]; + assert(Boolean(presetData), 'Preset "' + presetName + '" does not exist'); + var presetResult = this._processConfig(presetData); + Object.keys(presetResult).forEach(function(key) { + ruleSettings[key] = presetResult[key]; + }); + } + + // File extensions + if (config.fileExtensions) { + assert( + typeof config.fileExtensions === 'string' || Array.isArray(config.fileExtensions), + '`fileExtensions` option requires string or array value' + ); + this._fileExtensions = [].concat(config.fileExtensions).map(function(ext) { + return ext.toLowerCase(); + }); + } + + // File excludes + if (config.excludeFiles) { + assert(Array.isArray(config.excludeFiles), '`excludeFiles` option requires array value'); + this._excludedFileMasks = config.excludeFiles; + this._excludedFileMatchers = this._excludedFileMasks.map(function(fileMask) { + return new minimatch.Minimatch(path.resolve(this._basePath, fileMask), { + dot: true + }); + }, this); + } + + // Additional rules + if (config.additionalRules) { + assert(Array.isArray(config.additionalRules), '`additionalRules` option requires array value'); + config.additionalRules.forEach(this._loadAdditionalRule, this); + } + + if (config.hasOwnProperty('maxErrors')) { + assert( + typeof config.maxErrors === 'number' || config.maxErrors === null, + '`maxErrors` option requires number or null value' + ); + this._maxErrors = config.maxErrors; + } + + if (config.hasOwnProperty('esnext')) { + assert( + typeof config.esnext === 'boolean' || config.esnext === null, + '`esnext` option requires boolean or null value' + ); + this._esnextEnabled = Boolean(config.esnext); + } + + // Apply config options + Object.keys(config).forEach(function(key) { + if (!BUILTIN_OPTIONS[key]) { + ruleSettings[key] = config[key]; + } + }); + + return ruleSettings; +}; + +/** + * Loads plugin data. + * + * @param {function(Configuration)} plugin + * @protected + */ +Configuration.prototype._loadPlugin = function(plugin) { + assert(typeof plugin === 'function', '`plugin` should be a function'); + plugin(this); +}; + +/** + * Includes plugin in the configuration environment. + * + * @param {function(Configuration)|*} plugin + */ +Configuration.prototype.usePlugin = function(plugin) { + this._loadPlugin(plugin); +}; + +/** + * Loads additional rule. + * + * @param {Rule} additionalRule + * @protected + */ +Configuration.prototype._loadAdditionalRule = function(additionalRule) { + assert(typeof additionalRule === 'object', '`additionalRule` should be an object'); + this.registerRule(additionalRule); +}; + +/** + * Throws error for non camel-case options. + * + * @param {Object} ruleSettings + */ +Configuration.prototype._throwNonCamelCaseErrorIfNeeded = function(ruleSettings) { + function symbolToUpperCase(s, symbol) { + return symbol.toUpperCase(); + } + function fixSettings(originalSettings) { + var result = {}; + Object.keys(originalSettings).forEach(function(key) { + var camelCaseName = key.replace(/_([a-zA-Z])/g, symbolToUpperCase); + var value = originalSettings[key]; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + value = fixSettings(value); + } + result[camelCaseName] = value; + }); + return result; + } + + Object.keys(ruleSettings).forEach(function(key) { + if (key.indexOf('_') !== -1) { + throw new Error( + 'JSCS now accepts configuration options in camel case. Sorry for inconvenience. ' + + 'On the bright side, we tried to convert your jscs config to camel case.\n' + + '----------------------------------------\n' + + JSON.stringify(fixSettings(ruleSettings), null, 4) + + '\n----------------------------------------\n' + ); + } + }); +}; + +/** + * Adds rule to the collection. + * + * @param {Rule} rule + */ +Configuration.prototype.registerRule = function(rule) { + var optionName = rule.getOptionName(); + assert(!this._rules.hasOwnProperty(optionName), 'Rule "' + optionName + '" is already registered'); + this._rules[optionName] = rule; +}; + +/** + * Returns list of registered rules. + * + * @returns {Rule[]} + */ +Configuration.prototype.getRegisteredRules = function() { + var rules = this._rules; + return Object.keys(rules).map(function(ruleOptionName) { + return rules[ruleOptionName]; + }); +}; + +/** + * Adds preset to the collection. + * + * @param {String} presetName + * @param {Object} presetConfig + */ +Configuration.prototype.registerPreset = function(presetName, presetConfig) { + this._presets[presetName] = presetConfig; +}; + +/** + * Returns registered presets object (key - preset name, value - preset content). + * + * @returns {Object} + */ +Configuration.prototype.getRegisteredPresets = function() { + return this._presets; +}; + +/** + * Returns `true` if preset with specified name exists. + * + * @param {String} presetName + * @return {Boolean} + */ +Configuration.prototype.hasPreset = function(presetName) { + return this._presets.hasOwnProperty(presetName); +}; + +/** + * Registers built-in Code Style cheking rules. + */ +Configuration.prototype.registerDefaultRules = function() { + + /* + Important! + These rules are linked explicitly to keep browser-version supported. + */ + + this.registerRule(new (require('../rules/require-curly-braces'))()); + this.registerRule(new (require('../rules/require-multiple-var-decl'))()); + this.registerRule(new (require('../rules/disallow-multiple-var-decl'))()); + this.registerRule(new (require('../rules/disallow-empty-blocks'))()); + this.registerRule(new (require('../rules/require-space-after-keywords'))()); + this.registerRule(new (require('../rules/require-space-before-keywords'))()); + this.registerRule(new (require('../rules/disallow-space-after-keywords'))()); + this.registerRule(new (require('../rules/disallow-space-before-keywords'))()); + this.registerRule(new (require('../rules/require-parentheses-around-iife'))()); + + /* deprecated rules */ + this.registerRule(new (require('../rules/require-left-sticked-operators'))()); + this.registerRule(new (require('../rules/disallow-left-sticked-operators'))()); + this.registerRule(new (require('../rules/require-right-sticked-operators'))()); + this.registerRule(new (require('../rules/disallow-right-sticked-operators'))()); + this.registerRule(new (require('../rules/validate-jsdoc'))()); + /* deprecated rules (end) */ + + this.registerRule(new (require('../rules/require-operator-before-line-break'))()); + this.registerRule(new (require('../rules/disallow-implicit-type-conversion'))()); + this.registerRule(new (require('../rules/require-camelcase-or-uppercase-identifiers'))()); + this.registerRule(new (require('../rules/disallow-keywords'))()); + this.registerRule(new (require('../rules/disallow-multiple-line-breaks'))()); + this.registerRule(new (require('../rules/disallow-multiple-line-strings'))()); + this.registerRule(new (require('../rules/validate-line-breaks'))()); + this.registerRule(new (require('../rules/validate-quote-marks'))()); + this.registerRule(new (require('../rules/validate-indentation'))()); + this.registerRule(new (require('../rules/disallow-trailing-whitespace'))()); + this.registerRule(new (require('../rules/disallow-mixed-spaces-and-tabs'))()); + this.registerRule(new (require('../rules/require-keywords-on-new-line'))()); + this.registerRule(new (require('../rules/disallow-keywords-on-new-line'))()); + this.registerRule(new (require('../rules/require-line-feed-at-file-end'))()); + this.registerRule(new (require('../rules/maximum-line-length'))()); + this.registerRule(new (require('../rules/require-yoda-conditions'))()); + this.registerRule(new (require('../rules/disallow-yoda-conditions'))()); + this.registerRule(new (require('../rules/require-spaces-inside-object-brackets'))()); + this.registerRule(new (require('../rules/require-spaces-inside-array-brackets'))()); + this.registerRule(new (require('../rules/require-spaces-inside-parentheses'))()); + this.registerRule(new (require('../rules/disallow-spaces-inside-object-brackets'))()); + this.registerRule(new (require('../rules/disallow-spaces-inside-array-brackets'))()); + this.registerRule(new (require('../rules/disallow-spaces-inside-parentheses'))()); + this.registerRule(new (require('../rules/require-blocks-on-newline'))()); + this.registerRule(new (require('../rules/require-space-after-object-keys'))()); + this.registerRule(new (require('../rules/require-space-before-object-values'))()); + this.registerRule(new (require('../rules/disallow-space-after-object-keys'))()); + this.registerRule(new (require('../rules/disallow-space-before-object-values'))()); + this.registerRule(new (require('../rules/disallow-quoted-keys-in-objects'))()); + this.registerRule(new (require('../rules/disallow-dangling-underscores'))()); + this.registerRule(new (require('../rules/require-aligned-object-values'))()); + + this.registerRule(new (require('../rules/disallow-padding-newlines-in-blocks'))()); + this.registerRule(new (require('../rules/require-padding-newlines-in-blocks'))()); + this.registerRule(new (require('../rules/require-padding-newlines-in-objects'))()); + this.registerRule(new (require('../rules/disallow-padding-newlines-in-objects'))()); + this.registerRule(new (require('../rules/require-newline-before-block-statements'))()); + this.registerRule(new (require('../rules/disallow-newline-before-block-statements'))()); + + this.registerRule(new (require('../rules/require-padding-newlines-before-keywords'))()); + this.registerRule(new (require('../rules/disallow-padding-newlines-before-keywords'))()); + + this.registerRule(new (require('../rules/disallow-trailing-comma'))()); + this.registerRule(new (require('../rules/require-trailing-comma'))()); + + this.registerRule(new (require('../rules/disallow-comma-before-line-break'))()); + this.registerRule(new (require('../rules/require-comma-before-line-break'))()); + + this.registerRule(new (require('../rules/disallow-space-before-block-statements.js'))()); + this.registerRule(new (require('../rules/require-space-before-block-statements.js'))()); + + this.registerRule(new (require('../rules/disallow-space-before-postfix-unary-operators.js'))()); + this.registerRule(new (require('../rules/require-space-before-postfix-unary-operators.js'))()); + + this.registerRule(new (require('../rules/disallow-space-after-prefix-unary-operators.js'))()); + this.registerRule(new (require('../rules/require-space-after-prefix-unary-operators.js'))()); + + this.registerRule(new (require('../rules/disallow-space-before-binary-operators'))()); + this.registerRule(new (require('../rules/require-space-before-binary-operators'))()); + + this.registerRule(new (require('../rules/disallow-space-after-binary-operators'))()); + this.registerRule(new (require('../rules/require-space-after-binary-operators'))()); + + this.registerRule(new (require('../rules/require-spaces-in-conditional-expression'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-conditional-expression'))()); + + this.registerRule(new (require('../rules/require-spaces-in-function'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-function'))()); + this.registerRule(new (require('../rules/require-spaces-in-function-expression'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-function-expression'))()); + this.registerRule(new (require('../rules/require-spaces-in-anonymous-function-expression'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-anonymous-function-expression'))()); + this.registerRule(new (require('../rules/require-spaces-in-named-function-expression'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-named-function-expression'))()); + this.registerRule(new (require('../rules/require-spaces-in-function-declaration'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-function-declaration'))()); + + this.registerRule(new (require('../rules/require-spaces-in-call-expression'))()); + this.registerRule(new (require('../rules/disallow-spaces-in-call-expression'))()); + + this.registerRule(new (require('../rules/validate-parameter-separator'))()); + + this.registerRule(new (require('../rules/require-capitalized-constructors'))()); + + this.registerRule(new (require('../rules/safe-context-keyword'))()); + + this.registerRule(new (require('../rules/require-dot-notation'))()); + + this.registerRule(new (require('../rules/require-space-after-line-comment'))()); + this.registerRule(new (require('../rules/disallow-space-after-line-comment'))()); + + this.registerRule(new (require('../rules/require-anonymous-functions'))()); + this.registerRule(new (require('../rules/disallow-anonymous-functions'))()); + + this.registerRule(new (require('../rules/require-function-declarations'))()); + this.registerRule(new (require('../rules/disallow-function-declarations'))()); + + this.registerRule(new (require('../rules/require-capitalized-comments'))()); + this.registerRule(new (require('../rules/disallow-capitalized-comments'))()); +}; + +/** + * Registers built-in Code Style cheking presets. + */ +Configuration.prototype.registerDefaultPresets = function() { + // https://github.com/airbnb/javascript + this.registerPreset('airbnb', require('../../presets/airbnb.json')); + + // http://javascript.crockford.com/code.html + this.registerPreset('crockford', require('../../presets/crockford.json')); + + // https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml + this.registerPreset('google', require('../../presets/google.json')); + + // https://contribute.jquery.org/style-guide/js/ + this.registerPreset('jquery', require('../../presets/jquery.json')); + + // https://github.com/mrdoob/three.js/wiki/Mr.doob's-Code-Style%E2%84%A2 + this.registerPreset('mdcs', require('../../presets/mdcs.json')); + + // https://www.mediawiki.org/wiki/Manual:Coding_conventions/JavaScript + this.registerPreset('wikimedia', require('../../presets/wikimedia.json')); + + // https://github.com/ymaps/codestyle/blob/master/js.md + this.registerPreset('yandex', require('../../presets/yandex.json')); +}; + +module.exports = Configuration; diff --git a/lib/config/node-configuration.js b/lib/config/node-configuration.js new file mode 100644 index 000000000..f99554210 --- /dev/null +++ b/lib/config/node-configuration.js @@ -0,0 +1,60 @@ +var path = require('path'); +var util = require('util'); +var glob = require('glob'); +var Configuration = require('./configuration'); + +/** + * nodejs-compatible configuration module. + * + * @name NodeConfiguration + * @augments Configuration + * @constructor + */ +function NodeConfiguration() { + Configuration.call(this); + this._basePath = process.cwd(); +} + +util.inherits(NodeConfiguration, Configuration); + +/** + * Loads plugin data. + * + * @param {String|function(Configuration)} plugin + * @protected + */ +NodeConfiguration.prototype._loadPlugin = function(plugin) { + if (typeof plugin === 'string') { + var pluginPath = plugin; + if (isRelativeRequirePath(pluginPath)) { + pluginPath = path.resolve(this._basePath, pluginPath); + } + plugin = require(pluginPath); + } + Configuration.prototype._loadPlugin.call(this, plugin); +}; + +/** + * Loads additional rule. + * + * @param {String|Rule} additionalRule + * @protected + */ +NodeConfiguration.prototype._loadAdditionalRule = function(additionalRule) { + if (typeof additionalRule === 'string') { + glob.sync(path.resolve(this._basePath, additionalRule)).forEach(function(path) { + var Rule = require(path); + Configuration.prototype._loadAdditionalRule.call(this, new Rule()); + }, this); + } else { + Configuration.prototype._loadAdditionalRule.call(this, additionalRule); + } +}; + +function isRelativeRequirePath(requirePath) { + // Logic from: https://github.com/joyent/node/blob/4f1ae11a62b97052bc83756f8cb8700cc1f61661/lib/module.js#L237 + var start = requirePath.substring(0, 2); + return start === './' || start === '..'; +} + +module.exports = NodeConfiguration; diff --git a/lib/options/additional-rules.js b/lib/options/additional-rules.js deleted file mode 100644 index 15e18473f..000000000 --- a/lib/options/additional-rules.js +++ /dev/null @@ -1,16 +0,0 @@ -var path = require('path'); -var glob = require('glob'); - -module.exports = function(config, instance, cwd) { - (config.additionalRules || []).forEach(function(pattern) { - glob.sync(path.resolve(cwd, pattern)).map(function(path) { - var Rule = require(path); - instance.registerRule(new Rule()); - }); - }); - - Object.defineProperty(config, 'additionalRules', { - value: config.additionalRules, - enumerable: false - }); -}; diff --git a/lib/options/esnext.js b/lib/options/esnext.js new file mode 100644 index 000000000..4bb332eb7 --- /dev/null +++ b/lib/options/esnext.js @@ -0,0 +1,6 @@ +module.exports = function(config) { + Object.defineProperty(config, 'esnext', { + value: !!config.esnext, + enumerable: false + }); +}; diff --git a/lib/options/exclude-files.js b/lib/options/exclude-files.js deleted file mode 100644 index ccc44530e..000000000 --- a/lib/options/exclude-files.js +++ /dev/null @@ -1,15 +0,0 @@ -var path = require('path'); -var minimatch = require('minimatch'); - -module.exports = function(config, instance, cwd) { - instance._excludes = (config.excludeFiles || []).map(function(pattern) { - return new minimatch.Minimatch(path.resolve(cwd, pattern), { - dot: true - }); - }); - - Object.defineProperty(config, 'excludeFiles', { - value: config.excludeFiles, - enumerable: false - }); -}; diff --git a/lib/options/file-extensions.js b/lib/options/file-extensions.js deleted file mode 100644 index 461777509..000000000 --- a/lib/options/file-extensions.js +++ /dev/null @@ -1,20 +0,0 @@ -var DEFAULT_FILE_EXTENSIONS = ['.js']; - -module.exports = function(config, instance) { - if (typeof config.fileExtensions === 'string') { - instance._fileExtensions = [config.fileExtensions.toLowerCase()]; - } else if (Array.isArray(config.fileExtensions)) { - instance._fileExtensions = config.fileExtensions.map( - function(s) { - return s.toLowerCase(); - } - ); - } else { - instance._fileExtensions = DEFAULT_FILE_EXTENSIONS; - } - - Object.defineProperty(config, 'fileExtensions', { - value: config.fileExtensions, - enumerable: false - }); -}; diff --git a/lib/options/max-errors.js b/lib/options/max-errors.js deleted file mode 100644 index 2b4936254..000000000 --- a/lib/options/max-errors.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Limits the total number of errors reported - * - * @param {Object} config - * @param {StringСhecker} instance - */ -module.exports = function(config, instance) { - instance._maxErrors = config.maxErrors; - - Object.defineProperty(config, 'maxErrors', { - value: Number(config.maxErrors), - enumerable: false - }); -}; diff --git a/lib/options/preset.js b/lib/options/preset.js deleted file mode 100644 index 19f2dfa0a..000000000 --- a/lib/options/preset.js +++ /dev/null @@ -1,61 +0,0 @@ -var presets = { - // https://github.com/airbnb/javascript - airbnb: require('../../presets/airbnb.json'), - // http://javascript.crockford.com/code.html - crockford: require('../../presets/crockford.json'), - // https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml - google: require('../../presets/google.json'), - // https://contribute.jquery.org/style-guide/js/ - jquery: require('../../presets/jquery.json'), - // https://github.com/mrdoob/three.js/wiki/Mr.doob's-Code-Style%E2%84%A2 - mdcs: require('../../presets/mdcs.json'), - // https://www.mediawiki.org/wiki/Manual:Coding_conventions/JavaScript - wikimedia: require('../../presets/wikimedia.json'), - // https://github.com/ymaps/codestyle/blob/master/js.md - yandex: require('../../presets/yandex.json') -}; - -module.exports = { - /** - * Get does not exist error - * @param {String} preset - * @return {String} - */ - getDoesNotExistError: function(preset) { - return 'Preset "' + preset + '" does not exist'; - }, - /** - * Is preset exists in jscs - * @param {String} preset - * @return {Boolean} - */ - exists: function(preset) { - return !!presets[preset]; - }, - - /** - * Extend jscs config with preset rules - * @param {Object} config - * @return {Boolean} - */ - extend: function(config) { - if (!config.preset) { - return true; - } - - var preset = presets[config.preset]; - - if (!preset) { - return false; - } - - delete config.preset; - for (var rule in preset) { - if (preset.hasOwnProperty(rule) && !(rule in config)) { - config[rule] = preset[rule]; - } - } - - return true; - } -}; diff --git a/lib/rules/disallow-dangling-underscores.js b/lib/rules/disallow-dangling-underscores.js index e67502092..5614206b0 100644 --- a/lib/rules/disallow-dangling-underscores.js +++ b/lib/rules/disallow-dangling-underscores.js @@ -4,21 +4,41 @@ module.exports = function() {}; module.exports.prototype = { - configure: function(disallowDanglingUnderscores) { + configure: function(identifiers) { assert( - typeof disallowDanglingUnderscores === 'boolean', - 'disallowDanglingUnderscores option requires boolean value' + identifiers === true || + typeof identifiers === 'object', + this.getOptionName() + ' option requires the value `true` ' + + 'or an object with String[] `allExcept` property' ); + + // verify first item in `allExcept` property in object (if it's an object) assert( - disallowDanglingUnderscores === true, - 'disallowDanglingUnderscores option requires true value or should be removed' + typeof identifiers !== 'object' || + Array.isArray(identifiers.allExcept) && + typeof identifiers.allExcept[0] === 'string', + 'Property `allExcept` in requireSpaceAfterLineComment should be an array of strings' ); - this._allowedIdentifiers = { - _: true, - __dirname: true, - __filename: true - }; + var isTrue = identifiers === true; + var defaultIdentifiers = [ + '__proto__', + '_', + '__dirname', + '__filename', + 'super_' + ]; + + if (isTrue) { + identifiers = defaultIdentifiers; + } else { + identifiers = (identifiers.allExcept).concat(defaultIdentifiers); + } + + this._identifierIndex = {}; + for (var i = 0, l = identifiers.length; i < l; i++) { + this._identifierIndex[identifiers[i]] = true; + } }, getOptionName: function() { @@ -26,7 +46,7 @@ module.exports.prototype = { }, check: function(file, errors) { - var allowedIdentifiers = this._allowedIdentifiers; + var allowedIdentifiers = this._identifierIndex; file.iterateTokensByType('Identifier', function(token) { var value = token.value; diff --git a/lib/rules/disallow-padding-newlines-before-keywords.js b/lib/rules/disallow-padding-newlines-before-keywords.js new file mode 100644 index 000000000..29ef8ebe4 --- /dev/null +++ b/lib/rules/disallow-padding-newlines-before-keywords.js @@ -0,0 +1,43 @@ +var assert = require('assert'); +var defaultKeywords = require('../utils').spacedKeywords; + +module.exports = function() { }; + +module.exports.prototype = { + + configure: function(keywords) { + assert(Array.isArray(keywords) || keywords === true, + 'disallowPaddingNewlinesBeforeKeywords option requires array or true value'); + + if (keywords === true) { + keywords = defaultKeywords; + } + + this._keywordIndex = {}; + for (var i = 0, l = keywords.length; i < l; i++) { + this._keywordIndex[keywords[i]] = true; + } + }, + + getOptionName: function() { + return 'disallowPaddingNewlinesBeforeKeywords'; + }, + + check: function(file, errors) { + var keywordIndex = this._keywordIndex; + + file.iterateTokensByType('Keyword', function(token, i, tokens) { + if (keywordIndex[token.value]) { + var prevToken = tokens[i - 1]; + + if (prevToken && token.loc.start.line - prevToken.loc.end.line > 1) { + errors.add( + 'Keyword `' + token.value + '` should not have an empty line above it', + token.loc.start.line, + token.loc.start.column + ); + } + } + }); + } +}; diff --git a/lib/rules/disallow-space-before-keywords.js b/lib/rules/disallow-space-before-keywords.js new file mode 100644 index 000000000..c3c85e211 --- /dev/null +++ b/lib/rules/disallow-space-before-keywords.js @@ -0,0 +1,71 @@ +var assert = require('assert'); +var util = require('util'); +var texts = [ + 'Illegal space before "%s" keyword', + 'Should be zero spaces instead of %d, before "%s" keyword' +]; + +var defaultKeywords = require('../utils').spacedKeywords; + +module.exports = function() {}; + +module.exports.prototype = { + + configure: function(keywords) { + assert( + Array.isArray(keywords) || keywords === true, + 'disallowSpaceBeforeKeywords option requires array or true value'); + + if (keywords === true) { + keywords = defaultKeywords; + } + + this._keywordIndex = {}; + for (var i = 0, l = keywords.length; i < l; i++) { + this._keywordIndex[keywords[i]] = true; + } + }, + + getOptionName: function() { + return 'disallowSpaceBeforeKeywords'; + }, + + check: function(file, errors) { + var keywordIndex = this._keywordIndex; + + function getCommentIfExists(start, end) { + return file.getComments().filter(function(comment) { + return start <= comment.range[0] && end >= comment.range[1]; + })[0]; + } + + file.iterateTokensByType(['Keyword'], function(token, i, tokens) { + if (keywordIndex[token.value]) { + var prevToken = tokens[i - 1]; + if (!prevToken) { + return; + } + + var comment = getCommentIfExists(prevToken.range[1], token.range[0]); + prevToken = comment || prevToken; + + var diff = token.range[0] - prevToken.range[1]; + + if (prevToken.loc.end.line === token.loc.start.line && diff !== 0) { + if (prevToken.type !== 'Punctuator' || prevToken.value !== ';') { + errors.add( + util.format.apply(null, + diff === 1 ? + [texts[0], token.value] : + [texts[1], diff, token.value] + ), + token.loc.start.line, + token.loc.start.column + ); + } + } + } + }); + } + +}; diff --git a/lib/rules/disallow-spaces-in-anonymous-function-expression.js b/lib/rules/disallow-spaces-in-anonymous-function-expression.js index 57f4c4c09..b7e6fa1b1 100644 --- a/lib/rules/disallow-spaces-in-anonymous-function-expression.js +++ b/lib/rules/disallow-spaces-in-anonymous-function-expression.js @@ -67,7 +67,7 @@ module.exports.prototype = { if (!nextToken) { errors.add( 'Illegal space before opening round brace', - functionToken.loc.start + functionToken.loc.end ); } } diff --git a/lib/rules/require-padding-newlines-before-keywords.js b/lib/rules/require-padding-newlines-before-keywords.js new file mode 100644 index 000000000..a14a9772d --- /dev/null +++ b/lib/rules/require-padding-newlines-before-keywords.js @@ -0,0 +1,54 @@ +var assert = require('assert'); +var defaultKeywords = require('../utils').spacedKeywords; + +module.exports = function() { }; + +module.exports.prototype = { + + configure: function(keywords) { + assert(Array.isArray(keywords) || keywords === true, + 'requirePaddingNewlinesBeforeKeywords option requires array or true value'); + + if (keywords === true) { + keywords = defaultKeywords; + } + + this._keywordIndex = {}; + for (var i = 0, l = keywords.length; i < l; i++) { + this._keywordIndex[keywords[i]] = true; + } + }, + + getOptionName: function() { + return 'requirePaddingNewlinesBeforeKeywords'; + }, + + check: function(file, errors) { + var keywordIndex = this._keywordIndex; + + file.iterateTokensByType('Keyword', function(token, i, tokens) { + if (keywordIndex[token.value]) { + var prevToken = tokens[i - 1]; + + // Handle special case of 'else if' construct. + if (token.value === 'if' && prevToken && prevToken.value === 'else') { + return; + } + + // Handle all other cases + // The { character is there to handle the case of a matching token which happens to be the first + // statement in a block + // The ) character is there to handle the case of `if (...) matchingKeyword` in which case + // requiring padding would break the statement + if (prevToken && prevToken.value !== '{' && prevToken.value !== ')' && + token.loc.start.line - prevToken.loc.end.line < 2) { + errors.add( + 'Keyword `' + token.value + '` should have an empty line above it', + token.loc.start.line, + token.loc.start.column + ); + } + } + }); + } +}; diff --git a/lib/rules/require-space-after-keywords.js b/lib/rules/require-space-after-keywords.js index cb5d88187..b38c7fb6d 100644 --- a/lib/rules/require-space-after-keywords.js +++ b/lib/rules/require-space-after-keywords.js @@ -54,7 +54,7 @@ module.exports.prototype = { if (nextToken.type !== 'Punctuator' || nextToken.value !== ';') { errors.add( util.format.apply(null, - diff === 1 ? + diff === 0 ? [texts[0], token.value] : [texts[1], diff, token.value] ), diff --git a/lib/rules/require-space-after-line-comment.js b/lib/rules/require-space-after-line-comment.js index 845c8414d..1a281106c 100644 --- a/lib/rules/require-space-after-line-comment.js +++ b/lib/rules/require-space-after-line-comment.js @@ -7,11 +7,25 @@ module.exports.prototype = { configure: function(requireSpaceAfterLineComment) { assert( requireSpaceAfterLineComment === true || - requireSpaceAfterLineComment === 'allowSlash', - 'requireSpaceAfterLineComment option requires the value true or `allowSlash`' + requireSpaceAfterLineComment === 'allowSlash' || + typeof requireSpaceAfterLineComment === 'object', + 'requireSpaceAfterLineComment option requires the value `true` ' + + 'or an object with String[] `allExcept` property' ); - this._allowSlash = requireSpaceAfterLineComment === 'allowSlash'; + // verify first item in `allExcept` property in object (if it's an object) + assert( + typeof requireSpaceAfterLineComment !== 'object' || + Array.isArray(requireSpaceAfterLineComment.allExcept) && + typeof requireSpaceAfterLineComment.allExcept[0] === 'string', + 'Property `allExcept` in requireSpaceAfterLineComment should be an array of strings' + ); + + // don't check triple slashed comments, microsoft js doc convention. see #593 + // exceptions. see #592 + // need to drop allowSlash support in 2.0. Fixes #697 + this._allExcept = requireSpaceAfterLineComment === 'allowSlash' ? ['/'] : + requireSpaceAfterLineComment.allExcept || []; }, getOptionName: function() { @@ -20,16 +34,18 @@ module.exports.prototype = { check: function(file, errors) { var comments = file.getComments(); - var allowSlash = this._allowSlash; + var allExcept = this._allExcept; comments.forEach(function(comment) { if (comment.type === 'Line') { var value = comment.value; - // don't check triple slashed comments, microsoft js doc convention. see #593 - if (allowSlash && value[0] === '/') { - value = value.substr(1); - } + // cutout exceptions + allExcept.forEach(function(el) { + if (value.indexOf(el) === 0) { + value = value.substr(el.length); + } + }); if (value.length === 0) { return; diff --git a/lib/rules/require-space-before-keywords.js b/lib/rules/require-space-before-keywords.js new file mode 100644 index 000000000..6cf7a6197 --- /dev/null +++ b/lib/rules/require-space-before-keywords.js @@ -0,0 +1,71 @@ +var assert = require('assert'); +var util = require('util'); +var texts = [ + 'Missing space before "%s" keyword', + 'Should be one space instead of %d, before "%s" keyword' +]; + +var defaultKeywords = require('../utils').spacedKeywords; + +module.exports = function() {}; + +module.exports.prototype = { + + configure: function(keywords) { + assert( + Array.isArray(keywords) || keywords === true, + 'requireSpaceAfterKeywords option requires array or true value'); + + if (keywords === true) { + keywords = defaultKeywords; + } + + this._keywordIndex = {}; + for (var i = 0, l = keywords.length; i < l; i++) { + this._keywordIndex[keywords[i]] = true; + } + }, + + getOptionName: function() { + return 'requireSpaceBeforeKeywords'; + }, + + check: function(file, errors) { + var keywordIndex = this._keywordIndex; + + function getCommentIfExists(start, end) { + return file.getComments().filter(function(comment) { + return start <= comment.range[0] && end >= comment.range[1]; + })[0]; + } + + file.iterateTokensByType(['Keyword'], function(token, i, tokens) { + if (keywordIndex[token.value]) { + var prevToken = tokens[i - 1]; + if (!prevToken) { + return; + } + + var comment = getCommentIfExists(prevToken.range[1], token.range[0]); + prevToken = comment || prevToken; + + var diff = token.range[0] - prevToken.range[1]; + + if (prevToken.loc.end.line === token.loc.start.line && diff !== 1) { + if (prevToken.type !== 'Punctuator' || prevToken.value !== ';') { + errors.add( + util.format.apply(null, + diff === 0 ? + [texts[0], token.value] : + [texts[1], diff, token.value] + ), + token.loc.start.line, + token.loc.start.column + ); + } + } + } + }); + } + +}; diff --git a/lib/rules/validate-indentation.js b/lib/rules/validate-indentation.js index 6674ead5d..ed0e50054 100644 --- a/lib/rules/validate-indentation.js +++ b/lib/rules/validate-indentation.js @@ -47,8 +47,32 @@ module.exports.prototype = { return firstContent; } - function markPop(node, indents) { - linesToCheck[node.loc.end.line - 1].pop = indents; + function markPop(node, outdents) { + linesToCheck[node.loc.end.line - 1].pop = outdents; + } + + function markExtraPops(node, count) { + linesToCheck[node.loc.end.line - 1].extraPops = count; + } + + function markCasePop(caseNode, children) { + var outdentNode = getCaseOutdent(children); + var outdents; + + // If a case statement has a `break` or `return` as a direct child and it is the + // first one encountered, use it as the example for all future case indentation + if (outdentNode && _this._breakIndents === null) { + _this._breakIndents = (caseNode.loc.start.column === outdentNode.loc.start.column) ? 1 : 0; + } + + // If a case statement has no `break` nor `return` as a direct child + // (e.g. an if nested in a case statement), mark that there is an extra + // pop because there is no statement that "closes" the case statement + if (!outdentNode) { + markExtraPops(caseNode, 1); + } else { + markPop(caseNode, _this._breakIndents); + } } function markPushAndCheck(pushNode, indents) { @@ -96,6 +120,7 @@ module.exports.prototype = { var outdent = indentSize * line.pop; var expected; var idx = indentStack.length - 1; + var extraPops = line.extraPops; if (line.pop === null) { expected = indentStack[idx]; @@ -113,12 +138,14 @@ module.exports.prototype = { // when the expected is an array, resolve the value // back into a Number by checking both values are the actual indentation if (line.check && Array.isArray(expected)) { - if (actual === expected[1]) { - indentStack[idx] = expected[1]; - } else { - indentStack[idx] = expected[0]; + expected = actual === expected[1] ? expected[1] : expected[0]; + if (!line.pop) { + indentStack[idx] = expected; } - expected = indentStack[idx]; + } + + while (extraPops--) { + indentStack.pop(); } return expected; @@ -146,20 +173,16 @@ module.exports.prototype = { } } - function getBreakIndents(caseNode, children) { - var breakNode; - if (_this._breakIndents === null) { - children.some(function(node) { - if (node.type === 'BreakStatement') { - breakNode = node; - return true; - } - }); - - _this._breakIndents = (caseNode.loc.start.column === breakNode.loc.start.column) ? 1 : 0; - } + function getCaseOutdent(caseChildren) { + var outdentNode; + caseChildren.some(function(node) { + if (node.type === 'BreakStatement' || node.type === 'ReturnStatement') { + outdentNode = node; + return true; + } + }); - return _this._breakIndents; + return outdentNode; } function generateIndentations() { @@ -245,7 +268,7 @@ module.exports.prototype = { if (children.length > 1 || (children[0] && children[0].type !== 'BlockStatement')) { markChildren(node); - markPop(node, getBreakIndents(node, children)); + markCasePop(node, children); markPushAndCheck(node, 1); } else if (children.length === 0) { linesToCheck[node.loc.start.line - 1].push = 1; @@ -298,7 +321,8 @@ module.exports.prototype = { push: null, pushAltLine: null, pop: null, - check: null + check: null, + extraPops: 0 }; }); diff --git a/lib/rules/validate-parameter-separator.js b/lib/rules/validate-parameter-separator.js index 170818a72..dae502dbc 100644 --- a/lib/rules/validate-parameter-separator.js +++ b/lib/rules/validate-parameter-separator.js @@ -48,6 +48,11 @@ module.exports.prototype = { 'Missing space after function parameter \'' + param.name + '\'', punctuatorToken.loc.start ); + } else if (separatorBefore === ' ' && (punctuatorToken.range[0] - param.range[1]) > 1) { + errors.add( + 'Unexpected space before function parameter \'' + param.name + '\'', + param.loc.start + ); } if (separatorAfter === ',' && @@ -63,6 +68,11 @@ module.exports.prototype = { 'Missing space before function parameter \'' + nextParamToken.value + '\'', nextParamToken.loc.start ); + } else if (separatorAfter === ' ' && (nextParamToken.range[0] - punctuatorToken.range[1]) > 1) { + errors.add( + 'Unexpected space before function parameter \'' + nextParamToken.value + '\'', + nextParamToken.loc.start + ); } } }); diff --git a/lib/string-checker.js b/lib/string-checker.js index f4a9faf59..3f56964fc 100644 --- a/lib/string-checker.js +++ b/lib/string-checker.js @@ -1,22 +1,34 @@ -var esprima = require('esprima'); +var defaultEsprima = require('esprima'); +var harmonyEsprima = require('esprima-harmony-jscs'); var Errors = require('./errors'); var JsFile = require('./js-file'); -var preset = require('./options/preset'); -var maxErrors = require('./options/max-errors'); +var Configuration = require('./config/configuration'); /** * Starts Code Style checking process. * * @name StringChecker - * @param {Boolean} verbose + * @param {Boolean|Object} options either a boolean flag representing verbosity (deprecated), or an options object + * @param {Boolean} options.verbose true adds the rule name to the error messages it produces, false does not + * @param {Boolean} options.esnext true attempts to parse the code as es6, false does not + * @param {Object} options.esprima if provided, will be used to parse source code instead of the built-in esprima parser */ -var StringChecker = function(verbose) { - this._rules = []; - this._activeRules = []; - this._config = {}; - this._verbose = verbose || false; +var StringChecker = function(options) { + this._configuredRules = []; + this._errorsFound = 0; this._maxErrorsExceeded = false; + + this._configuration = this._createConfiguration(); + this._configuration.registerDefaultPresets(); + + if (typeof options === 'boolean') { + this._verbose = options; + } else { + options = options || {}; + this._verbose = options.verbose || false; + this._esprima = options.esprima || (options.esnext ? harmonyEsprima : defaultEsprima); + } }; StringChecker.prototype = { @@ -26,125 +38,14 @@ StringChecker.prototype = { * @param {Rule} rule */ registerRule: function(rule) { - this._rules.push(rule); + this._configuration.registerRule(rule); }, /** - * Registers built-in Code Style cheking rules. + * Registers built-in Code Style checking rules. */ registerDefaultRules: function() { - this.registerRule(new (require('./rules/require-curly-braces'))()); - this.registerRule(new (require('./rules/require-multiple-var-decl'))()); - this.registerRule(new (require('./rules/disallow-multiple-var-decl'))()); - this.registerRule(new (require('./rules/disallow-empty-blocks'))()); - this.registerRule(new (require('./rules/require-space-after-keywords'))()); - this.registerRule(new (require('./rules/disallow-space-after-keywords'))()); - this.registerRule(new (require('./rules/require-parentheses-around-iife'))()); - - /* deprecated rules */ - this.registerRule(new (require('./rules/require-left-sticked-operators'))()); - this.registerRule(new (require('./rules/disallow-left-sticked-operators'))()); - this.registerRule(new (require('./rules/require-right-sticked-operators'))()); - this.registerRule(new (require('./rules/disallow-right-sticked-operators'))()); - this.registerRule(new (require('./rules/validate-jsdoc'))()); - /* deprecated rules (end) */ - - this.registerRule(new (require('./rules/require-operator-before-line-break'))()); - this.registerRule(new (require('./rules/disallow-implicit-type-conversion'))()); - this.registerRule(new (require('./rules/require-camelcase-or-uppercase-identifiers'))()); - this.registerRule(new (require('./rules/disallow-keywords'))()); - this.registerRule(new (require('./rules/disallow-multiple-line-breaks'))()); - this.registerRule(new (require('./rules/disallow-multiple-line-strings'))()); - this.registerRule(new (require('./rules/validate-line-breaks'))()); - this.registerRule(new (require('./rules/validate-quote-marks'))()); - this.registerRule(new (require('./rules/validate-indentation'))()); - this.registerRule(new (require('./rules/disallow-trailing-whitespace'))()); - this.registerRule(new (require('./rules/disallow-mixed-spaces-and-tabs'))()); - this.registerRule(new (require('./rules/require-keywords-on-new-line'))()); - this.registerRule(new (require('./rules/disallow-keywords-on-new-line'))()); - this.registerRule(new (require('./rules/require-line-feed-at-file-end'))()); - this.registerRule(new (require('./rules/maximum-line-length'))()); - this.registerRule(new (require('./rules/require-yoda-conditions'))()); - this.registerRule(new (require('./rules/disallow-yoda-conditions'))()); - this.registerRule(new (require('./rules/require-spaces-inside-object-brackets'))()); - this.registerRule(new (require('./rules/require-spaces-inside-array-brackets'))()); - this.registerRule(new (require('./rules/require-spaces-inside-parentheses'))()); - this.registerRule(new (require('./rules/disallow-spaces-inside-object-brackets'))()); - this.registerRule(new (require('./rules/disallow-spaces-inside-array-brackets'))()); - this.registerRule(new (require('./rules/disallow-spaces-inside-parentheses'))()); - this.registerRule(new (require('./rules/require-blocks-on-newline'))()); - this.registerRule(new (require('./rules/require-space-after-object-keys'))()); - this.registerRule(new (require('./rules/require-space-before-object-values'))()); - this.registerRule(new (require('./rules/disallow-space-after-object-keys'))()); - this.registerRule(new (require('./rules/disallow-space-before-object-values'))()); - this.registerRule(new (require('./rules/disallow-quoted-keys-in-objects'))()); - this.registerRule(new (require('./rules/disallow-dangling-underscores'))()); - this.registerRule(new (require('./rules/require-aligned-object-values'))()); - - this.registerRule(new (require('./rules/disallow-padding-newlines-in-blocks'))()); - this.registerRule(new (require('./rules/require-padding-newlines-in-blocks'))()); - this.registerRule(new (require('./rules/require-padding-newlines-in-objects'))()); - this.registerRule(new (require('./rules/disallow-padding-newlines-in-objects'))()); - this.registerRule(new (require('./rules/require-newline-before-block-statements'))()); - this.registerRule(new (require('./rules/disallow-newline-before-block-statements'))()); - - this.registerRule(new (require('./rules/disallow-trailing-comma'))()); - this.registerRule(new (require('./rules/require-trailing-comma'))()); - - this.registerRule(new (require('./rules/disallow-comma-before-line-break'))()); - this.registerRule(new (require('./rules/require-comma-before-line-break'))()); - - this.registerRule(new (require('./rules/disallow-space-before-block-statements.js'))()); - this.registerRule(new (require('./rules/require-space-before-block-statements.js'))()); - - this.registerRule(new (require('./rules/disallow-space-before-postfix-unary-operators.js'))()); - this.registerRule(new (require('./rules/require-space-before-postfix-unary-operators.js'))()); - - this.registerRule(new (require('./rules/disallow-space-after-prefix-unary-operators.js'))()); - this.registerRule(new (require('./rules/require-space-after-prefix-unary-operators.js'))()); - - this.registerRule(new (require('./rules/disallow-space-before-binary-operators'))()); - this.registerRule(new (require('./rules/require-space-before-binary-operators'))()); - - this.registerRule(new (require('./rules/disallow-space-after-binary-operators'))()); - this.registerRule(new (require('./rules/require-space-after-binary-operators'))()); - - this.registerRule(new (require('./rules/require-spaces-in-conditional-expression'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-conditional-expression'))()); - - this.registerRule(new (require('./rules/require-spaces-in-function'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-function'))()); - this.registerRule(new (require('./rules/require-spaces-in-function-expression'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-function-expression'))()); - this.registerRule(new (require('./rules/require-spaces-in-anonymous-function-expression'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-anonymous-function-expression'))()); - this.registerRule(new (require('./rules/require-spaces-in-named-function-expression'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-named-function-expression'))()); - this.registerRule(new (require('./rules/require-spaces-in-function-declaration'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-function-declaration'))()); - - this.registerRule(new (require('./rules/require-spaces-in-call-expression'))()); - this.registerRule(new (require('./rules/disallow-spaces-in-call-expression'))()); - - this.registerRule(new (require('./rules/validate-parameter-separator'))()); - - this.registerRule(new (require('./rules/require-capitalized-constructors'))()); - - this.registerRule(new (require('./rules/safe-context-keyword'))()); - - this.registerRule(new (require('./rules/require-dot-notation'))()); - - this.registerRule(new (require('./rules/require-space-after-line-comment'))()); - this.registerRule(new (require('./rules/disallow-space-after-line-comment'))()); - - this.registerRule(new (require('./rules/require-anonymous-functions'))()); - this.registerRule(new (require('./rules/disallow-anonymous-functions'))()); - - this.registerRule(new (require('./rules/require-function-declarations'))()); - this.registerRule(new (require('./rules/disallow-function-declarations'))()); - - this.registerRule(new (require('./rules/require-capitalized-comments'))()); - this.registerRule(new (require('./rules/disallow-capitalized-comments'))()); + this._configuration.registerDefaultRules(); }, /** @@ -152,7 +53,7 @@ StringChecker.prototype = { * @return {Object} */ getProcessedConfig: function() { - return this._config; + return this._configuration.getProcessedConfig(); }, /** @@ -161,77 +62,14 @@ StringChecker.prototype = { * @param {Object} config */ configure: function(config) { - maxErrors(config, this); - - this.throwNonCamelCaseErrorIfNeeded(config); - - if (config.preset && !preset.exists(config.preset)) { - throw new Error(preset.getDoesNotExistError(config.preset)); - } - - preset.extend(config); + this._configuration.load(config); - var configRules = Object.keys(config); - var activeRules = this._activeRules; - - this._config = config; - this._rules.forEach(function(rule) { - var ruleOptionName = rule.getOptionName(); - - if (config.hasOwnProperty(ruleOptionName)) { - - // Do not configure the rule if it's equals to null (#203) - if (config[ruleOptionName] !== null) { - rule.configure(config[ruleOptionName]); - } - activeRules.push(rule); - configRules.splice(configRules.indexOf(ruleOptionName), 1); - } - }); - if (configRules.length > 0) { - throw new Error('Unsupported rules: ' + configRules.join(', ')); + if (this._configuration.isESNextEnabled()) { + this._esprima = harmonyEsprima; } - }, - /** - * Throws error for non camel-case options. - * - * @param {Object} config - */ - throwNonCamelCaseErrorIfNeeded: function(config) { - function symbolToUpperCase(s, symbol) { - return symbol.toUpperCase(); - } - function fixConfig(originConfig) { - var result = {}; - for (var i in originConfig) { - if (originConfig.hasOwnProperty(i)) { - var camelCaseName = i.replace(/_([a-zA-Z])/g, symbolToUpperCase); - var value = originConfig[i]; - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - value = fixConfig(value); - } - result[camelCaseName] = value; - } - } - return result; - } - var hasOldStyleConfigParams = false; - for (var i in config) { - if (config.hasOwnProperty(i)) { - if (i.indexOf('_') !== -1) { - hasOldStyleConfigParams = true; - break; - } - } - } - if (hasOldStyleConfigParams) { - throw new Error('JSCS now accepts configuration options in camel case. Sorry for inconvenience. ' + - 'On the bright side, we tried to convert your jscs config to camel case.\n' + - '----------------------------------------\n' + - JSON.stringify(fixConfig(config), null, 4) + - '\n----------------------------------------\n'); - } + this._configuredRules = this._configuration.getConfiguredRules(); + this._maxErrors = this._configuration.getMaxErrors(); }, /** @@ -248,7 +86,7 @@ StringChecker.prototype = { var parseError; try { - tree = esprima.parse(str, {loc: true, range: true, comment: true, tokens: true}); + tree = this._esprima.parse(str, {loc: true, range: true, comment: true, tokens: true}); } catch (e) { parseError = e; } @@ -263,12 +101,10 @@ StringChecker.prototype = { return errors; } - this._activeRules.forEach(function(rule) { + this._configuredRules.forEach(function(rule) { // Do not process the rule if it's equals to null (#203) - if (this._config[rule.getOptionName()] !== null) { - errors.setCurrentRule(rule.getOptionName()); - rule.check(file, errors); - } + errors.setCurrentRule(rule.getOptionName()); + rule.check(file, errors); }, this); // sort errors list to show errors as they appear in source @@ -276,7 +112,7 @@ StringChecker.prototype = { return (a.line - b.line) || (a.column - b.column); }); - if (this._maxErrors !== undefined) { + if (this._maxErrors !== null && !isNaN(this._maxErrors)) { if (!this._maxErrorsExceeded) { this._maxErrorsExceeded = this._errorsFound + errors.getErrorCount() > this._maxErrors; } @@ -296,6 +132,25 @@ StringChecker.prototype = { */ maxErrorsExceeded: function() { return this._maxErrorsExceeded; + }, + + /** + * Returns new configuration instance. + * + * @protected + * @returns {Configuration} + */ + _createConfiguration: function() { + return new Configuration(); + }, + + /** + * Returns current configuration instance. + * + * @returns {Configuration} + */ + getConfiguration: function() { + return this._configuration; } }; diff --git a/package.json b/package.json index b79f91443..a0c0529d4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Marat Dulin ", "description": "JavaScript Code Style", "name": "jscs", - "version": "1.6.2", + "version": "1.7.3", "main": "lib/checker", "homepage": "https://github.com/jscs-dev/node-jscs", "license": "MIT", @@ -26,9 +26,10 @@ "node": ">= 0.10.0" }, "dependencies": { - "colors": "~0.6.2", - "commander": "~2.3.0", + "colors": "~1.0.3", + "commander": "~2.5.0", "esprima": "~1.2.2", + "esprima-harmony-jscs": "1.1.0-dev-harmony", "exit": "~0.1.2", "glob": "~4.0.0", "minimatch": "~1.0.0", @@ -36,24 +37,24 @@ "vow": "~0.4.3", "vow-fs": "~0.3.1", "xmlbuilder": "~2.4.0", - "supports-color": "~1.1.0" + "supports-color": "~1.2.0" }, "devDependencies": { - "browserify": "~5.10.0", + "browserify": "~6.2.0", "coveralls": "~2.11.1", "has-ansi": "~1.0.0", "hooker": "~0.2.3", "jshint": "~2.5.0", - "mocha": "~1.21.4", + "mocha": "~2.0.1", "rewire": "~2.1.0", - "separated-coverage": "~2.3.0", - "sinon": "~1.10.0", + "unit-coverage": "~3.0.0", + "sinon": "~1.11.0", "xml2js": "~0.4.2" }, "bin": { "jscs": "./bin/jscs" }, - "separated-coverage": { + "unit-coverage": { "common": [ "-a", "lib", @@ -74,13 +75,13 @@ "scripts": { "lint": "jshint . && node bin/jscs lib test bin publish", "test": "npm run lint && mocha", - "coverage": "scov run -p common", - "coverage-html": "scov run -p common -r html -o coverage.html", + "coverage": "unit-coverage run -p common", + "coverage-html": "unit-coverage run -p common -r html -o coverage.html", "browserify": "browserify --standalone JscsStringChecker lib/string-checker.js -o jscs-browser.js", "changelog": "git log `git describe --tags --abbrev=0`..HEAD --pretty=format:' * %s (%an)' | grep -v 'Merge pull request'", "release": "node publish/prepublish && npm publish", "postpublish": "node publish/postpublish", - "travis": "npm run test && scov run -p common -r lcov -o out.lcov && cat out.lcov | coveralls" + "travis": "npm run test && unit-coverage run -p common -r lcov -o out.lcov && cat out.lcov | coveralls" }, "files": [ "bin", diff --git a/presets/crockford.json b/presets/crockford.json index cbbf2662f..2da465312 100644 --- a/presets/crockford.json +++ b/presets/crockford.json @@ -49,7 +49,6 @@ "requireSpaceAfterBinaryOperators": true, "requireCamelCaseOrUpperCaseIdentifiers": true, "disallowKeywords": [ "with" ], - "disallowMultipleLineBreaks": true, "validateQuoteMarks": "'", "validateIndentation": 4, "disallowMixedSpacesAndTabs": true, diff --git a/test/checker.js b/test/checker.js index 96e4df6cc..e78e922a0 100644 --- a/test/checker.js +++ b/test/checker.js @@ -14,23 +14,13 @@ describe('modules/checker', function() { }); describe('checkFile', function() { - afterEach(function() { - if (checker._isExcluded.restore) { - checker._isExcluded.restore(); - } - }); - it('should check for exclusion', function() { - sinon.spy(checker, '_isExcluded'); - - checker.checkFile('./test/data/checker/file.js'); - - assert(checker._isExcluded.called); - }); it('should return empty array of errors for excluded files', function() { - sinon.stub(checker, '_isExcluded', function() { - return true; + checker = new Checker(); + checker.registerDefaultRules(); + checker.configure({ + disallowKeywords: ['with'], + excludeFiles: ['./test/**'] }); - return checker.checkFile('./test/data/checker/file.js').then(function(errors) { assert(errors === null); }); diff --git a/test/cli.js b/test/cli.js index 7dd23bba7..be4547dad 100644 --- a/test/cli.js +++ b/test/cli.js @@ -20,6 +20,7 @@ describe('modules/cli', function() { sinon.stub(process.stdout, 'write'); sinon.stub(process.stderr, 'write'); }); + afterEach(function() { process.chdir(startingDir); @@ -59,7 +60,8 @@ describe('modules/cli', function() { }); return result.promise.fail(function() { - assert(console.error.getCall(0).args[0] === 'Preset "not-exist" does not exist'); + assert(console.error.getCall(0).args[0] === 'Preset "%s" does not exist'); + assert(console.error.getCall(0).args[1] === 'not-exist'); console.error.restore(); }); }); @@ -104,14 +106,15 @@ describe('modules/cli', function() { it('should set presets', function() { var Checker = require('../lib/checker'); - var old = Checker.prototype.checkPath; + var originalCheckPath = Checker.prototype.checkPath; + + function restoreCheckPath() { + Checker.prototype.checkPath = originalCheckPath; + } Checker.prototype.checkPath = function(path) { assert(path, 'test/data/cli/success.js'); - - Checker.prototype.checkPath = old; - - return Vow.defer().promise(); + return originalCheckPath.apply(this, arguments); }; var result = cli({ @@ -119,8 +122,13 @@ describe('modules/cli', function() { preset: 'jquery', config: 'test/data/cli/cli.json' }); - - assert(result.checker.getProcessedConfig().requireCurlyBraces); + return result.promise.then(function() { + assert(result.checker.getProcessedConfig().requireCurlyBraces); + restoreCheckPath(); + }).fail(function(e) { + restoreCheckPath(); + throw e; + }); }); it('should bail out if no inputs files are specified', function() { diff --git a/test/config/configuration.js b/test/config/configuration.js new file mode 100644 index 000000000..451388297 --- /dev/null +++ b/test/config/configuration.js @@ -0,0 +1,394 @@ +var assert = require('assert'); +var sinon = require('sinon'); +var Configuration = require('../../lib/config/configuration'); + +describe('modules/config/configuration', function() { + + var configuration; + beforeEach(function() { + configuration = new Configuration(); + }); + + describe('constructor', function() { + it('should set default base path', function() { + assert(configuration.getBasePath() === '.'); + }); + + it('should set default file extensions', function() { + assert(configuration.getFileExtensions().length === 1); + assert(configuration.getFileExtensions()[0] === '.js'); + }); + + it('should have no default registered rules', function() { + assert(configuration.getRegisteredRules().length === 0); + }); + + it('should have no default configured rules', function() { + assert(configuration.getConfiguredRules().length === 0); + }); + + it('should have no default presets', function() { + assert(Object.keys(configuration.getRegisteredPresets()).length === 0); + }); + + it('should have no default excluded file masks', function() { + assert(configuration.getExcludedFileMasks().length === 0); + }); + + it('should have no default maximal error count', function() { + assert(configuration.getMaxErrors() === null); + }); + }); + + describe('registerRule', function() { + it('should add rule to registered rule list', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + } + }; + configuration.registerRule(rule); + assert(configuration.getRegisteredRules().length === 1); + assert(configuration.getRegisteredRules()[0] === rule); + assert(configuration.getConfiguredRules().length === 0); + }); + + it('should fail on duplicate rule name', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + } + }; + configuration.registerRule(rule); + try { + configuration.registerRule(rule); + assert(false); + } catch (e) { + assert(e.message === 'Rule "ruleName" is already registered'); + } + }); + }); + + describe('getRegisteredRules', function() { + it('should return registered rule list', function() { + var rule1 = { + getOptionName: function() { + return 'ruleName1'; + } + }; + var rule2 = { + getOptionName: function() { + return 'ruleName2'; + } + }; + configuration.registerRule(rule1); + configuration.registerRule(rule2); + assert(configuration.getRegisteredRules().length === 2); + assert(configuration.getRegisteredRules()[0] === rule1); + assert(configuration.getRegisteredRules()[1] === rule2); + }); + }); + + describe('getRegisteredPresets', function() { + it('should return registered presets object', function() { + var preset = {maxErrors: 5}; + assert(Object.keys(configuration.getRegisteredPresets()).length === 0); + configuration.registerPreset('company', preset); + assert(Object.keys(configuration.getRegisteredPresets()).length === 1); + assert(configuration.getRegisteredPresets().company === preset); + }); + }); + + describe('hasPreset', function() { + it('should return true if preset presents in collection', function() { + var preset = {maxErrors: 5}; + assert(!configuration.hasPreset('company')); + configuration.registerPreset('company', preset); + assert(configuration.hasPreset('company')); + }); + }); + + describe('registerDefaultRules', function() { + it('should register built-in rules', function() { + configuration.registerDefaultRules(); + var optionNames = configuration.getRegisteredRules().map(function(rule) { + return rule.getOptionName(); + }); + + // checking for some of them + assert(optionNames.indexOf('requireCurlyBraces') !== -1); + assert(optionNames.indexOf('disallowEmptyBlocks') !== -1); + }); + }); + + describe('registerDefaultPresets', function() { + it('should register built-in presets', function() { + assert(!configuration.hasPreset('jquery')); + configuration.registerDefaultPresets(); + assert(configuration.hasPreset('airbnb')); + assert(configuration.hasPreset('crockford')); + assert(configuration.hasPreset('google')); + assert(configuration.hasPreset('jquery')); + assert(configuration.hasPreset('mdcs')); + assert(configuration.hasPreset('wikimedia')); + assert(configuration.hasPreset('yandex')); + }); + }); + + describe('getConfiguredRules', function() { + it('should return configured rules after config load', function() { + assert(configuration.getConfiguredRules().length === 0); + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.registerRule(rule); + configuration.load({ruleName: true}); + assert(configuration.getConfiguredRules().length === 1); + assert(configuration.getConfiguredRules()[0] === rule); + }); + }); + + describe('isFileExcluded', function() { + it('should return `false` if no `excludeFiles` are defined', function() { + assert(!configuration.isFileExcluded('1.js')); + assert(!configuration.isFileExcluded('')); + assert(!configuration.isFileExcluded('*')); + }); + + it('should return `true` for excluded file', function() { + configuration.load({excludeFiles: ['1.js', 'app/1.js']}); + assert(configuration.isFileExcluded('1.js')); + assert(configuration.isFileExcluded('app/1.js')); + assert(!configuration.isFileExcluded('share/1.js')); + assert(!configuration.isFileExcluded('2.js')); + assert(!configuration.isFileExcluded('')); + }); + + it('should return resolve given path', function() { + configuration.load({excludeFiles: ['app/1.js']}); + assert(configuration.isFileExcluded('app/lib/../1.js')); + }); + }); + + describe('usePlugin', function() { + it('should run plugin with configuration specified', function() { + var plugin = function() {}; + var spy = sinon.spy(plugin); + configuration.usePlugin(spy); + assert(spy.called); + assert(spy.callCount === 1); + assert(spy.getCall(0).args[0] === configuration); + }); + }); + + describe('override', function() { + it('should override `preset` setting', function() { + configuration.registerPreset('1', {}); + configuration.registerPreset('2', {}); + configuration.override({preset: '2'}); + configuration.load({preset: '1'}); + assert(configuration.getProcessedConfig().preset === '2'); + }); + + it('should override `maxErrors` setting', function() { + configuration.override({maxErrors: 2}); + configuration.load({maxErrors: 1}); + assert(configuration.getProcessedConfig().maxErrors === 2); + }); + }); + + describe('load', function() { + it('should configure rules', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + var configureSpy = sinon.spy(rule, 'configure'); + configuration.registerRule(rule); + configuration.load({ruleName: true}); + assert(configuration.getProcessedConfig().ruleName === true); + assert(configureSpy.callCount === 1); + assert(configureSpy.getCall(0).args.length === 1); + assert(configureSpy.getCall(0).args[0] === true); + }); + + it('should throw error on unsupported rule', function() { + try { + configuration.load({ruleName: true}); + assert(false); + } catch (e) { + assert.equal(e.message, 'Unsupported rules: ruleName'); + } + }); + + it('should throw error on list of unsupported rules', function() { + try { + configuration.load({ruleName1: true, ruleName2: true}); + assert(false); + } catch (e) { + assert.equal(e.message, 'Unsupported rules: ruleName1, ruleName2'); + } + }); + + it('should not configure rule on null', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + var configureSpy = sinon.spy(rule, 'configure'); + configuration.registerRule(rule); + configuration.load({ruleName: null}); + assert(!configuration.getProcessedConfig().hasOwnProperty('ruleName')); + assert(configureSpy.callCount === 0); + }); + + it('should load `preset` options', function() { + configuration.registerPreset('preset', {maxErrors: 1}); + configuration.load({preset: 'preset'}); + assert(configuration.getProcessedConfig().preset === 'preset'); + assert(configuration.getProcessedConfig().maxErrors === 1); + }); + + it('should load `preset` rule settings', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.registerRule(rule); + configuration.registerPreset('preset', {ruleName: true}); + configuration.load({preset: 'preset'}); + assert(configuration.getProcessedConfig().preset === 'preset'); + assert(configuration.getProcessedConfig().ruleName === true); + }); + + it('should accept `maxErrors` number', function() { + configuration.load({maxErrors: 1}); + assert(configuration.getMaxErrors() === 1); + }); + + it('should accept `maxErrors` null', function() { + configuration.load({maxErrors: null}); + assert(configuration.getMaxErrors() === null); + }); + + it('should accept `excludeFiles`', function() { + configuration.load({excludeFiles: ['**']}); + assert(configuration.getExcludedFileMasks().length === 1); + assert(configuration.getExcludedFileMasks()[0] === '**'); + }); + + it('should accept `fileExtensions` array', function() { + configuration.load({fileExtensions: ['.jsx']}); + assert(configuration.getFileExtensions().length === 1); + assert(configuration.getFileExtensions()[0] === '.jsx'); + }); + + it('should accept `fileExtensions` string', function() { + configuration.load({fileExtensions: '.jsx'}); + assert(configuration.getFileExtensions().length === 1); + assert(configuration.getFileExtensions()[0] === '.jsx'); + }); + + it('should accept `additionalRules` to register rules', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.load({additionalRules: [rule]}); + assert(configuration.getRegisteredRules().length === 1); + assert(configuration.getRegisteredRules()[0] === rule); + assert(configuration.getConfiguredRules().length === 0); + }); + + it('should accept `additionalRules` to configure rules', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.load({additionalRules: [rule], ruleName: true}); + assert(configuration.getConfiguredRules().length === 1); + assert(configuration.getConfiguredRules()[0] === rule); + }); + + it('should accept `configPath`', function() { + configuration.load({configPath: 'app/1.js'}); + assert(configuration.getBasePath() === 'app'); + }); + + it('should accept `plugins`', function() { + var plugin = function() {}; + var spy = sinon.spy(plugin); + configuration.load({plugins: [spy]}); + assert(spy.called); + assert(spy.callCount === 1); + assert(spy.getCall(0).args[0] === configuration); + }); + + it('should thow non-camelcase error for underscore-config', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.registerRule(rule); + try { + configuration.load({'rule_name': true}); + assert(false); + } catch (e) { + assert.equal( + e.message, + 'JSCS now accepts configuration options in camel case. ' + + 'Sorry for inconvenience. ' + + 'On the bright side, we tried to convert your jscs config to camel case.\n' + + '----------------------------------------\n' + + '{\n' + + ' "ruleName": true\n' + + '}\n' + + '----------------------------------------\n' + ); + } + }); + + it('should thow non-camelcase error with converted sub-configs', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.registerRule(rule); + try { + configuration.load({'rule_name': {'config_key': true}}); + assert(false); + } catch (e) { + assert.equal( + e.message, + 'JSCS now accepts configuration options in camel case. ' + + 'Sorry for inconvenience. ' + + 'On the bright side, we tried to convert your jscs config to camel case.\n' + + '----------------------------------------\n' + + '{\n' + + ' "ruleName": {\n' + + ' "configKey": true\n' + + ' }\n' + + '}\n' + + '----------------------------------------\n' + ); + } + }); + }); +}); diff --git a/test/config/node-configuration.js b/test/config/node-configuration.js new file mode 100644 index 000000000..ba96b85b3 --- /dev/null +++ b/test/config/node-configuration.js @@ -0,0 +1,80 @@ +var assert = require('assert'); +var path = require('path'); +var sinon = require('sinon'); +var NodeConfiguration = require('../../lib/config/node-configuration'); + +var AdditionalRule = require('../data/rules/additional-rules'); +var examplePluginSpy = require('../data/plugin/plugin'); + +describe('modules/config/node-configuration', function() { + var configuration; + beforeEach(function() { + configuration = new NodeConfiguration(); + }); + + describe('constructor', function() { + it('should set default base path to process.cwd()', function() { + assert(configuration.getBasePath() === process.cwd()); + }); + }); + + describe('load', function() { + it('should accept `additionalRules` to register rule instances', function() { + var rule = { + getOptionName: function() { + return 'ruleName'; + }, + configure: function() {} + }; + configuration.load({additionalRules: [rule]}); + assert(configuration.getRegisteredRules().length === 1); + assert(configuration.getRegisteredRules()[0] === rule); + }); + + it('should accept `additionalRules` to register rule paths', function() { + configuration.load({ + additionalRules: ['rules/additional-rules.js'], + configPath: path.resolve(__dirname + '/../data/config.json') + }); + assert(configuration.getRegisteredRules().length === 1); + assert(configuration.getRegisteredRules()[0] instanceof AdditionalRule); + }); + + it('should accept `additionalRules` to register rule path masks', function() { + configuration.load({ + additionalRules: ['rules/*.js'], + configPath: path.resolve(__dirname + '/../data/config.json') + }); + assert(configuration.getRegisteredRules().length === 1); + assert(configuration.getRegisteredRules()[0] instanceof AdditionalRule); + }); + + it('should accept `plugins` to register plugin instance', function() { + var plugin = function() {}; + var spy = sinon.spy(plugin); + configuration.load({plugins: [spy]}); + assert(spy.called); + assert(spy.callCount === 1); + assert(spy.getCall(0).args[0] === configuration); + }); + + it('should accept `plugins` to register plugin absolute path', function() { + configuration.load({plugins: [path.resolve(__dirname + '/../data/plugin/plugin')]}); + assert(examplePluginSpy.called); + assert(examplePluginSpy.callCount === 1); + assert(examplePluginSpy.getCall(0).args[0] === configuration); + examplePluginSpy.reset(); + }); + + it('should accept `plugins` to register plugin relative path', function() { + configuration.load({ + configPath: path.resolve(__dirname + '/../data/config.json'), + plugins: ['./plugin/plugin'] + }); + assert(examplePluginSpy.called); + assert(examplePluginSpy.callCount === 1); + assert(examplePluginSpy.getCall(0).args[0] === configuration); + examplePluginSpy.reset(); + }); + }); +}); diff --git a/test/data/options/preset/crockford.js b/test/data/options/preset/crockford.js index 34c953c57..3e1f3024c 100644 --- a/test/data/options/preset/crockford.js +++ b/test/data/options/preset/crockford.js @@ -42,6 +42,7 @@ function getElementsByClassName(className) { return results; } + div.onclick = function (e) { return false; }; diff --git a/test/data/plugin/plugin.js b/test/data/plugin/plugin.js new file mode 100644 index 000000000..09059ed97 --- /dev/null +++ b/test/data/plugin/plugin.js @@ -0,0 +1,2 @@ +var sinon = require('sinon'); +module.exports = sinon.spy(function(configuration) {}); diff --git a/test/data/validate-indentation.js b/test/data/validate-indentation.js index 140e0b2bb..82f4b1c4e 100644 --- a/test/data/validate-indentation.js +++ b/test/data/validate-indentation.js @@ -487,3 +487,32 @@ a++; // -> b++; c++; // <- } + +function c(d) { + return { + e: function(f, g) { + } + }; +} + +function a(b) { + switch(x) { + case 1: + if (foo) { + return 5; + } + } +} + +function a(b) { + switch(x) { + case 1: + c; + } +} + +function a(b) { + switch(x) { + case 1: c; + } +} diff --git a/test/mocha.opts b/test/mocha.opts index 4ebbf346b..3dfe217d3 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,3 @@ -test/*.js test/rules/*.js test/options/*.js test/reporters/*.js +test/*.js test/rules/*.js test/reporters/*.js test/config/*.js -R dot -u bdd diff --git a/test/options/additional-rules.js b/test/options/additional-rules.js deleted file mode 100644 index 79fc7c1a0..000000000 --- a/test/options/additional-rules.js +++ /dev/null @@ -1,39 +0,0 @@ -var Checker = require('../../lib/checker'); -var configFile = require('../../lib/cli-config'); -var assert = require('assert'); - -describe('options/additional-rules', function() { - var checker; - - beforeEach(function() { - checker = new Checker(); - }); - - it('should add additional rules', function() { - checker.configure({ - additionalRules: ['test/data/rules/*.js'], - testAdditionalRules: true - }); - - assert(checker.checkString('').getErrorCount() === 1); - }); - - it('should resolve rules path relative to config location', function() { - var checker = new Checker(); - checker.configure(configFile.load('./test/data/configs/additionalRules/.jscs.json')); - - assert(checker.checkString('').getErrorCount() === 1); - }); - - it('should be present in config after initialization', function() { - checker.configure({ - additionalRules: ['test/data/rules/*.js'], - testAdditionalRules: true - }); - - var config = checker.getProcessedConfig(); - - assert(config.additionalRules !== undefined); - assert(Object.getOwnPropertyDescriptor(config, 'additionalRules').enumerable === false); - }); -}); diff --git a/test/options/exclude-files.js b/test/options/exclude-files.js deleted file mode 100644 index 8652c63de..000000000 --- a/test/options/exclude-files.js +++ /dev/null @@ -1,174 +0,0 @@ -var Checker = require('../../lib/checker'); -var configFile = require('../../lib/cli-config'); -var assert = require('assert'); -var Vow = require('vow'); - -describe('options/exclude-files', function() { - var checker; - - beforeEach(function() { - checker = new Checker(); - checker.registerDefaultRules(); - }); - - describe('use config in script', function() { - it('should not report any errors', function() { - checker.configure({ - excludeFiles: ['test/data/configs/excludeFiles/exclude-files.js'], - disallowKeywords: ['with'] - }); - - return checker.checkFile('./test/data/configs/excludeFiles/exclude-files.js').then(function(errors) { - assert(errors === null); - }); - }); - - it('should allow patterns to match filenames starting with a period', function() { - checker.configure({ - excludeFiles: ['test/data/configs/excludeFiles/**'], - disallowKeywords: ['with'] - }); - - return checker.checkFile('./test/data/configs/excludeFiles/.withdot/error.js').then(function(errors) { - assert(errors === null); - }); - }); - - it('should resolve pattern to process.cwd', function() { - var results = []; - checker.configure({ - excludeFiles: ['test/data/exclude-files.js'], - disallowKeywords: ['with'] - }); - - // errors - results.push(checker.checkFile('./test/data/configs/excludeFiles/script.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - results.push(checker.checkFile('./test/data/configs/excludeFiles/nested/script.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - - return Vow.allResolved(results); - }); - - it('should resolve pattern to process.cwd', function() { - var results = []; - checker.configure({ - excludeFiles: ['test/data/exclude-files.js'], - disallowKeywords: ['with'] - }); - - // errors - results.push(checker.checkFile('./test/data/configs/excludeFiles/script.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - results.push(checker.checkFile('./test/data/configs/excludeFiles/nested/script.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - - return Vow.allResolved(results); - }); - }); - - describe('should resolve pattern relative to config file', function() { - it('(pattern: *.js)', function() { - var results = []; - checker.configure(configFile.load('./test/data/configs/excludeFiles/test1.jscs.json')); - - // ok - results.push(checker.checkFile('./test/data/configs/excludeFiles/exclude-files.js').then(function(errors) { - assert(errors === null); - })); - - // errors - results.push(checker.checkFile('./test/data/exclude-files.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - results.push(checker.checkFile('./test/data/configs/excludeFiles/nested/exclude-files.js') - .then(function(errors) { - assert(errors === null); - }) - ); - - return Vow.allResolved(results); - }); - - it('(pattern: exclude-files.js)', function() { - var results = []; - checker.configure(configFile.load('./test/data/configs/excludeFiles/test2.jscs.json')); - - // ok - results.push(checker.checkFile('./test/data/configs/excludeFiles/exclude-files.js').then(function(errors) { - assert(errors === null); - })); - - // errors - results.push(checker.checkFile('./test/data/exclude-files.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - results.push(checker.checkFile('./test/data/configs/excludeFiles/nested/exclude-files.js') - .then(function(errors) { - assert(errors === null); - }) - ); - - return Vow.allResolved(results); - }); - - it('(pattern: */exclude-files.js)', function() { - var results = []; - checker.configure(configFile.load('./test/data/configs/excludeFiles/test3.jscs.json')); - - // ok - results.push(checker.checkFile('./test/data/configs/excludeFiles/nested/exclude-files.js') - .then(function(errors) { - assert(errors === null); - }) - ); - - // errors - results.push(checker.checkFile('./test/data/exclude-files.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - results.push(checker.checkFile('./test/data/configs/excludeFiles/exclude-files.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - - return Vow.allResolved(results); - }); - - it('(pattern: ../**/exclude-files.js)', function() { - var results = []; - checker.configure(configFile.load('./test/data/configs/excludeFiles/test4.jscs.json')); - - // ok - results.push(checker.checkFile('./test/data/configs/excludeFiles/nested/exclude-files.js') - .then(function(errors) { - assert(errors === null); - }) - ); - results.push(checker.checkFile('./test/data/configs/excludeFiles/exclude-files.js').then(function(errors) { - assert(errors === null); - })); - - // errors - results.push(checker.checkFile('./test/data/exclude-files.js').then(function(errors) { - assert(errors.getErrorList().length === 0); - })); - - return Vow.allResolved(results); - }); - }); - - it('should be present in config after initialization', function() { - checker.configure({ - excludeFiles: [] - }); - - var config = checker.getProcessedConfig(); - - assert(config.excludeFiles !== undefined); - assert(Object.getOwnPropertyDescriptor(config, 'excludeFiles').enumerable === false); - }); -}); diff --git a/test/options/file-extensions.js b/test/options/file-extensions.js deleted file mode 100644 index d054e9ad9..000000000 --- a/test/options/file-extensions.js +++ /dev/null @@ -1,124 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -var Checker = require('../../lib/checker'); -var assert = require('assert'); - -describe('options/file-extensions', function() { - var checker; - - beforeEach(function() { - checker = new Checker(); - checker.registerDefaultRules(); - }); - - describe('default config', function() { - beforeEach(function() { - checker.configure({ - disallowKeywords: ['with'] - }); - }); - - it('should report errors for matching extensions (case insensitive) in directory with default config', - function() { - return checker.checkDirectory('./test/data/options/file-extensions').then(function(errors) { - assert(errors.length === 2); - }); - } - ); - }); - - describe('custom config', function() { - it('should report errors for matching extensions with custom config', function() { - checker.configure({ - fileExtensions: ['.jsx'], - disallowKeywords: ['with'] - }); - - return checker.checkDirectory('./test/data/options/file-extensions').then(function(errors) { - assert(errors.length === 1); - }); - }); - it('should report errors for matching extensions (case insensitive) with custom config', function() { - checker.configure({ - fileExtensions: ['.JS'], - disallowKeywords: ['with'] - }); - - return checker.checkDirectory('./test/data/options/file-extensions').then(function(errors) { - assert(errors.length === 2); - }); - }); - it('should report errors for matching extensions (case insensitive) with string value', function() { - checker.configure({ - fileExtensions: '.JS', - disallowKeywords: ['with'] - }); - - assert(checker.checkFile('./test/data/options/file-extensions/file-extensions-2.jS') !== null); - - return checker.checkDirectory('./test/data/options/file-extensions').then(function(errors) { - assert(errors.length === 2); - }); - }); - it('should report errors for matching extensions with custom config with multiple extensions', function() { - checker.configure({ - fileExtensions: ['.js', '.jsx'], - disallowKeywords: ['with'] - }); - - assert(checker.checkFile('./test/data/options/file-extensions/file-extensions.js') !== null); - assert(checker.checkFile('./test/data/options/file-extensions/file-extensions.jsx') !== null); - - return checker.checkDirectory('./test/data/options/file-extensions').then(function(errors) { - assert(errors.length === 3); - }); - }); - it('should report errors for matching extensions with Array *', function() { - var testPath = './test/data/options/file-extensions'; - - checker.configure({ - fileExtensions: ['*'], - disallowKeywords: ['with'] - }); - - return checker.checkDirectory(testPath).then(function(errors) { - assert(errors.length === fs.readdirSync(testPath).length); - }); - }); - it('should report errors for matching extensions with string *', function() { - var testPath = './test/data/options/file-extensions'; - - checker.configure({ - fileExtensions: '*', - disallowKeywords: ['with'] - }); - - return checker.checkDirectory(testPath).then(function(errors) { - assert(errors.length === fs.readdirSync(testPath).length); - }); - }); - - it('should report errors for file whose fullname is the same as matching extension', function() { - checker.configure({ - fileExtensions: 'file-extensions', - disallowKeywords: ['with'] - }); - - return checker.checkDirectory('./test/data/options/file-extensions').then(function(errors) { - assert(errors.length === 1); - }); - }); - }); - - it('should be present in config after initialization', function() { - checker.configure({ - fileExtensions: 'test' - }); - - var config = checker.getProcessedConfig(); - - assert(config.fileExtensions !== undefined); - assert(Object.getOwnPropertyDescriptor(config, 'fileExtensions').enumerable === false); - }); -}); diff --git a/test/options/preset.js b/test/options/preset.js deleted file mode 100644 index 4d03cd3a4..000000000 --- a/test/options/preset.js +++ /dev/null @@ -1,86 +0,0 @@ -var Checker = require('../../lib/checker'); -var preset = require('../../lib/options/preset'); -var assert = require('assert'); - -describe('options/preset', function() { - testPreset('airbnb'); - testPreset('crockford'); - testPreset('google'); - testPreset('jquery'); - testPreset('mdcs'); - testPreset('wikimedia'); - testPreset('yandex'); - - /** - * Helper to test a given preset's configuration against its test file - * - * Expects the given preset to have a configuration in /presets - * and real code taken from that project in /test/data/options/preset - * - * @example testPreset('google') - * @param {String} presetName - */ - function testPreset(presetName) { - describe(presetName + ' preset', function() { - var checker = new Checker(); - var preset = require('../../presets/' + presetName); - - checker.registerDefaultRules(); - checker.configure({ - preset: presetName - }); - - var config = checker.getProcessedConfig(); - - it('should set the correct rules', function() { - assert(config.requireCurlyBraces === preset.requireCurlyBraces); - assert(config.config !== preset); - }); - - it('should not report any errors from the sample file', function() { - return checker.checkFile('./test/data/options/preset/' + presetName + '.js').then(function(errors) { - assert(errors.isEmpty()); - }); - }); - }); - } - - describe('getDoesNotExistError', function() { - it('returns the correct error message', function() { - assert(preset.getDoesNotExistError('foo') === 'Preset "foo" does not exist'); - }); - }); - - describe('exists', function() { - it('returns true for existing presets', function() { - assert(preset.exists('jquery')); - }); - - it('returns false for non-existant presets', function() { - assert(!preset.exists('aPresetThatWillNeverExist')); - }); - }); - - describe('extend', function() { - it('returns true if preset not present in config', function() { - assert(preset.extend({ not: 'real' })); - }); - - it('returns false if provided preset is not a real preset', function() { - assert(!preset.extend({ preset: 'aPresetThatWillNeverExist' })); - }); - - it('removes the preset key from the config, and add its rules to the config', function() { - var config = { - preset: 'jquery', - fakeRule: true - }; - - preset.extend(config); - - assert(!config.preset); - assert(config.requireOperatorBeforeLineBreak); - assert(config.fakeRule); - }); - }); -}); diff --git a/test/rules/camel-case-options.js b/test/rules/camel-case-options.js deleted file mode 100644 index d6b2854a2..000000000 --- a/test/rules/camel-case-options.js +++ /dev/null @@ -1,40 +0,0 @@ -var Checker = require('../../lib/checker'); -var assert = require('assert'); - -describe('rules/camel-case-options', function() { - var checker; - beforeEach(function() { - checker = new Checker(); - checker.registerDefaultRules(); - }); - it('should report illegal option names', function() { - var error; - try { - checker.configure({ disallow_spaces_in_function_expression: { before_opening_round_brace: true } }); - } catch (e) { - error = e; - } - assert.equal( - error.message, - 'JSCS now accepts configuration options in camel case. ' + - 'Sorry for inconvenience. ' + - 'On the bright side, we tried to convert your jscs config to camel case.\n' + - '----------------------------------------\n' + - '{\n' + - ' "disallowSpacesInFunctionExpression": {\n' + - ' "beforeOpeningRoundBrace": true\n' + - ' }\n' + - '}\n' + - '----------------------------------------\n' - ); - }); - it('should not report legal option names', function() { - var error; - try { - checker.configure({ disallowSpacesInFunctionExpression: { beforeOpeningRoundBrace: true } }); - } catch (e) { - error = e; - } - assert(error === undefined); - }); -}); diff --git a/test/rules/disallow-dangling-underscores.js b/test/rules/disallow-dangling-underscores.js index e73b2cb0f..92c09572f 100644 --- a/test/rules/disallow-dangling-underscores.js +++ b/test/rules/disallow-dangling-underscores.js @@ -7,39 +7,88 @@ describe('rules/disallow-dangling-underscores', function() { beforeEach(function() { checker = new Checker(); checker.registerDefaultRules(); - checker.configure({ disallowDanglingUnderscores: true }); }); - it('should report leading underscores', function() { - assert(checker.checkString('var _x = "x";').getErrorCount() === 1); - }); + describe('option value true', function() { + beforeEach(function() { + checker.configure({ disallowDanglingUnderscores: true }); + }); - it('should report trailing underscores', function() { - assert(checker.checkString('var x_ = "x";').getErrorCount() === 1); - }); + it('should report leading underscores', function() { + assert(checker.checkString('var _x = "x";').getErrorCount() === 1); + }); - it('should report trailing underscores in member expressions', function() { - assert(checker.checkString('var x = this._privateField;').getErrorCount() === 1); - assert(checker.checkString('var x = instance._protectedField;').getErrorCount() === 1); - }); + it('should report trailing underscores', function() { + assert(checker.checkString('var x_ = "x";').getErrorCount() === 1); + }); - it('should report trailing underscores', function() { - assert(checker.checkString('var x_ = "x";').getErrorCount() === 1); - }); + it('should report trailing underscores in member expressions', function() { + assert(checker.checkString('var x = this._privateField;').getErrorCount() === 1); + assert(checker.checkString('var x = instance._protectedField;').getErrorCount() === 1); + }); - it('should not report underscore.js', function() { - assert(checker.checkString('var extend = _.extend;').isEmpty()); - }); + it('should report trailing underscores', function() { + assert(checker.checkString('var x_ = "x";').getErrorCount() === 1); + }); + + it('should not report inner underscores', function() { + assert(checker.checkString('var x_y = "x";').isEmpty()); + }); - it('should not report node globals', function() { - assert(checker.checkString('var a = __dirname + __filename;').isEmpty()); + it('should not report no underscores', function() { + assert(checker.checkString('var xy = "x";').isEmpty()); + }); }); - it('should not report inner underscores', function() { - assert(checker.checkString('var x_y = "x";').isEmpty()); + describe('option value true: default exceptions', function() { + beforeEach(function() { + checker.configure({ disallowDanglingUnderscores: true }); + }); + + it('should not report the prototype property', function() { + assert(checker.checkString('var proto = obj.__proto__;').isEmpty()); + }); + + it('should not report underscore.js', function() { + assert(checker.checkString('var extend = _.extend;').isEmpty()); + }); + + it('should not report node globals', function() { + assert(checker.checkString('var a = __dirname + __filename;').isEmpty()); + }); + + it('should not report the super constructor reference created by node\'s util.inherits', function() { + assert(checker.checkString('Inheritor.super_.call(this);').isEmpty()); + }); }); - it('should not report no underscores', function() { - assert(checker.checkString('var xy = "x";').isEmpty()); + describe('exceptions', function() { + beforeEach(function() { + checker.configure({ disallowDanglingUnderscores: { allExcept: ['_test', 'test_', '_test_', '__test'] } }); + }); + + it('should not report default exceptions: underscore.js', function() { + assert(checker.checkString('var extend = _.extend;').isEmpty()); + }); + + it('should not report _test', function() { + assert(checker.checkString('var a = _test;').isEmpty()); + }); + + it('should not report test_', function() { + assert(checker.checkString('var a = test_;').isEmpty()); + }); + + it('should not report _test_', function() { + assert(checker.checkString('var a = _test_;').isEmpty()); + }); + + it('should not report test__', function() { + assert(checker.checkString('var a = __test;').isEmpty()); + }); + + it('should report dangling underscore identifier that is not included in the array', function() { + assert(checker.checkString('var a = _notIncluded;').getErrorCount() === 1); + }); }); }); diff --git a/test/rules/disallow-padding-newlines-before-keywords.js b/test/rules/disallow-padding-newlines-before-keywords.js new file mode 100644 index 000000000..2f1d7a2f6 --- /dev/null +++ b/test/rules/disallow-padding-newlines-before-keywords.js @@ -0,0 +1,91 @@ +var Checker = require('../../lib/checker'); +var assert = require('assert'); + +describe('rules/disallow-padding-newlines-before-keywords', function() { + var checker; + + beforeEach(function() { + checker = new Checker(); + checker.registerDefaultRules(); + }); + + describe('array value', function() { + beforeEach(function() { + checker.configure({ + disallowPaddingNewlinesBeforeKeywords: ['if', 'for', 'return', 'switch', 'case', 'break', 'throw'] + }); + }); + + // Test simple case (including return statement check) + it('should report on matching return statement', function() { + assert( + checker.checkString( + 'function x() { var a;\n\nreturn; }' + ).getErrorCount() === 1 + ); + }); + + // Test cases for if statements + it('should report on matching if statement', function() { + assert( + checker.checkString( + 'function x() { var a = true;\n\nif (a) { a = !a; }; }' + ).getErrorCount() === 1 + ); + }); + + // Test case for 'for' statement + it('should report on matching for statement', function() { + assert( + checker.checkString( + 'function x() { var a = true;\n\nfor (var i = 0; i < 10; i++) { a = !a; }; }' + ).getErrorCount() === 1 + ); + }); + + // Test case for 'switch', 'case' and 'break' statement + it('should report on matching switch, case and break statements', function() { + assert( + checker.checkString( + 'function x() { var y = true;\n\nswitch ("Oranges") { case "Oranges": ' + + 'y = !y;\n\nbreak;\n\ncase "Apples": y = !y;\n\nbreak; default: y = !y; } }' + ).getErrorCount() === 4 + ); + }); + + // Test case for 'throw' statement + it('should report on matching throw statement', function() { + assert( + checker.checkString( + 'function x() {try { var a;\n\nthrow 0; } ' + + 'catch (e) { var b = 0;\n\nthrow e; } }' + ).getErrorCount() === 2 + ); + }); + + it('should report on multiple matching keywords', function() { + assert( + checker.checkString( + 'function x(a) { var b = 0;\n\nif (!a) { return false; };\n\n' + + 'for (var i = 0; i < b; i++) { if (!a[i]) return false; }\n\nreturn true; }' + ).getErrorCount() === 3 + ); + }); + }); + + describe('true value', function() { + beforeEach(function() { + checker.configure({ + disallowPaddingNewlinesBeforeKeywords: true + }); + }); + + it('should report on matching return statement', function() { + assert( + checker.checkString( + 'function x() { var a;\n\nreturn; }' + ).getErrorCount() === 1 + ); + }); + }); +}); diff --git a/test/rules/disallow-space-before-keywords.js b/test/rules/disallow-space-before-keywords.js new file mode 100644 index 000000000..96ab70fbd --- /dev/null +++ b/test/rules/disallow-space-before-keywords.js @@ -0,0 +1,71 @@ +var Checker = require('../../lib/checker'); +var assert = require('assert'); + +describe('rules/dissalow-space-before-keywords', function() { + var checker; + + beforeEach(function() { + checker = new Checker(); + checker.registerDefaultRules(); + }); + + it('should report illegal space before keyword', function() { + checker.configure({ disallowSpaceBeforeKeywords: ['else'] }); + + var errors = checker.checkString('if (true) {\n} else { x++; }'); + var error = errors.getErrorList()[0]; + + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Illegal space before "else" keyword') === 0); + }); + + it('should not report no space before keyword', function() { + checker.configure({ disallowSpaceBeforeKeywords: ['else'] }); + + assert(checker.checkString( + 'if (x) {\n' + + 'x++;\n' + + '}else {\n' + + 'x--;\n' + + '}' + ).isEmpty()); + }); + + it('should show different error if there is more than one space', function() { + checker.configure({ disallowSpaceBeforeKeywords: ['else'] }); + + var errors = checker.checkString('if (true) {\n} else { x++; }'); + var error = errors.getErrorList()[0]; + + assert(errors.explainError(error).indexOf('Should be zero spaces instead of 2, before "else" keyword') === 0); + }); + + it('should not trigger error for comments', function() { + checker.configure({ disallowSpaceBeforeKeywords: ['else'] }); + assert(checker.checkString('if (true) {\n} /**/else { x++; }').isEmpty()); + }); + + it('should report on all possible ES3 keywords if a value of true is supplied', function() { + checker.configure({ disallowSpaceBeforeKeywords: true }); + + var errors = checker.checkString('if (true) {\n} else { x++; }'); + var error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Illegal space before "else" keyword') === 0); + + errors = checker.checkString('/**/ if (true) {\n} else { x++; }'); + error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Illegal space before "else" keyword') === 0); + + errors = checker.checkString('do {\nx++;\n} while (x < 5)'); + error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Illegal space before "while" keyword') === 0); + + errors = checker.checkString('try {\nx++;\n} catch (e) {}'); + error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Illegal space before "catch" keyword') === 0); + }); +}); diff --git a/test/rules/disallow-spaces-in-anonymous-function-expression.js b/test/rules/disallow-spaces-in-anonymous-function-expression.js index 81f97aff1..bd817f3a4 100644 --- a/test/rules/disallow-spaces-in-anonymous-function-expression.js +++ b/test/rules/disallow-spaces-in-anonymous-function-expression.js @@ -36,6 +36,14 @@ describe('rules/disallow-spaces-in-anonymous-function-expression', function() { it('should not report missing space before round brace in setter expression', function() { assert(checker.checkString('var x = { set y(v) {} }').isEmpty()); }); + + it('should set correct pointer', function() { + var errors = checker.checkString('var x = function (){}'); + var error = errors.getErrorList()[0]; + + assert(errors.getErrorCount() === 1); + assert(error.column === 16); + }); }); describe('beforeOpeningCurlyBrace', function() { diff --git a/test/rules/require-padding-newlines-before-keywords.js b/test/rules/require-padding-newlines-before-keywords.js new file mode 100644 index 000000000..4841c7ccb --- /dev/null +++ b/test/rules/require-padding-newlines-before-keywords.js @@ -0,0 +1,123 @@ +var Checker = require('../../lib/checker'); +var assert = require('assert'); + +describe('rules/require-padding-newlines-before-keywords', function() { + var checker; + + beforeEach(function() { + checker = new Checker(); + checker.registerDefaultRules(); + }); + + describe('array value', function() { + beforeEach(function() { + checker.configure({ + requirePaddingNewlinesBeforeKeywords: ['if', 'for', 'return', 'switch', 'case', 'break', 'throw'] + }); + }); + + it('should not report on first expression in a block', function() { + assert( + checker.checkString( + 'function x() { return; }' + ).isEmpty() + ); + }); + + // Test simple case (including return statement check) + it('should report on matching return statement', function() { + assert( + checker.checkString( + 'function x() { var a; return; }' + ).getErrorCount() === 1 + ); + }); + + // Test cases for if statements + it('should report on matching if statement', function() { + assert( + checker.checkString( + 'function x() { var a = true; if (a) { a = !a; }; }' + ).getErrorCount() === 1 + ); + }); + + it('should not report on else if construct', function() { + assert( + checker.checkString( + 'if (true) {} else if (false) {}' + ).isEmpty() + ); + }); + + it('should not report on keyword following an if without curly braces', function() { + assert( + checker.checkString( + 'function x() { if (true) return; }' + ).isEmpty() + ); + }); + + // Test case for 'for' statement + it('should report on matching if statement', function() { + assert( + checker.checkString( + 'function x() { var a = true;' + + ' for (var i = 0; i < 10; i++) { a = !a; }; }' + ).getErrorCount() === 1 + ); + }); + + // Test case for 'switch', 'case' and 'break' statement + it('should report on matching if statement', function() { + assert( + checker.checkString( + 'function x() { var y = true; switch ("Oranges")' + ' { case "Oranges": y = !y; break;' + + ' case "Apples": y = !y; break; default: y = !y; } }' + ).getErrorCount() === 4 + ); + }); + + // Test case for 'throw' statement + it('should report on matching if statement', function() { + assert( + checker.checkString( + 'function x() {try { var a; throw 0; } catch (e) { var b = 0; throw e; } }' + ).getErrorCount() === 2 + ); + }); + + it('should report on multiple matching keywords', function() { + assert( + checker.checkString( + 'function x(a) { var b = 0; if (!a) { return false; };' + + ' for (var i = 0; i < b; i++) { if (!a[i]) return false; } return true; }' + ).getErrorCount() === 3 + ); + }); + }); + + describe('true value', function() { + beforeEach(function() { + checker.configure({ + requirePaddingNewlinesBeforeKeywords: true + }); + }); + + it('should not report on first expression in a block', function() { + assert( + checker.checkString( + 'function x() { return; }' + ).isEmpty() + ); + }); + + it('should report on matching return statement', function() { + assert( + checker.checkString( + 'function x() { var a; return; }' + ).getErrorCount() === 1 + ); + }); + }); +}); diff --git a/test/rules/require-space-after-keywords.js b/test/rules/require-space-after-keywords.js index 0e25db44a..73663f7b7 100644 --- a/test/rules/require-space-after-keywords.js +++ b/test/rules/require-space-after-keywords.js @@ -11,7 +11,10 @@ describe('rules/require-space-after-keywords', function() { it('should report missing space after keyword', function() { checker.configure({ requireSpaceAfterKeywords: ['if'] }); - assert(checker.checkString('if(x) { x++; }').getErrorCount() === 1); + var errors = checker.checkString('if(x) { x++; }'); + var error = errors.getErrorList()[0]; + + assert(errors.explainError(error).indexOf('Missing space after "if" keyword') === 0); }); it('should not report space after keyword', function() { diff --git a/test/rules/require-space-after-line-comment.js b/test/rules/require-space-after-line-comment.js index a4f211a29..5c96b1bd0 100644 --- a/test/rules/require-space-after-line-comment.js +++ b/test/rules/require-space-after-line-comment.js @@ -32,8 +32,13 @@ describe('rules/require-space-after-line-comment', function() { it('should report triple slashed comments', function() { assert(checker.checkString('if (true) {abc();} /// something').getErrorCount() === 1); }); + + it('should report sharped line comments', function() { + assert(checker.checkString('if (true) {abc();} //# something').getErrorCount() === 1); + }); }); + // deprecated. fixes #697 describe('option value allowSlash', function() { beforeEach(function() { checker.configure({ requireSpaceAfterLineComment: 'allowSlash' }); @@ -54,4 +59,29 @@ describe('rules/require-space-after-line-comment', function() { .isEmpty()); }); }); + + describe('exceptions #, --, (xsharp)', function() { + beforeEach(function() { + checker.configure({ requireSpaceAfterLineComment: { allExcept: ['#', '--', '(xsharp)'] } }); + }); + + it('should not report sharped comment', function() { + assert(checker.checkString('function area() {\n //# require something.js\n}') + .isEmpty()); + }); + + it('should not report (xsharp) line comment', function() { + assert(checker.checkString('function area() {\n //(xsharp) special comment\n}') + .isEmpty()); + }); + + it('should not report line comment with custom substrings', function() { + assert(checker.checkString('function area() {\n' + + ' //(xsharp) sourceURL=filename.js\n' + + ' //-- require something-else.js\n' + + ' return res;\n' + + '}') + .isEmpty()); + }); + }); }); diff --git a/test/rules/require-space-before-keywords.js b/test/rules/require-space-before-keywords.js new file mode 100644 index 000000000..0c88985bf --- /dev/null +++ b/test/rules/require-space-before-keywords.js @@ -0,0 +1,90 @@ +var Checker = require('../../lib/checker'); +var assert = require('assert'); + +describe('rules/require-space-before-keywords', function() { + var checker; + + beforeEach(function() { + checker = new Checker(); + checker.registerDefaultRules(); + }); + + it('should report missing space before keyword', function() { + checker.configure({ requireSpaceBeforeKeywords: ['else'] }); + + var errors = checker.checkString('if (true) {\n}else { x++; }'); + var error = errors.getErrorList()[0]; + + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Missing space before "else" keyword') === 0); + }); + + it('should not report space before keyword', function() { + checker.configure({ requireSpaceBeforeKeywords: ['else'] }); + + assert(checker.checkString( + 'if (x) {\n' + + 'x++;\n' + + '} else {\n' + + 'x--;\n' + + '}' + ).isEmpty()); + }); + + it('should not report space before non-coddled keywords', function() { + checker.configure({ requireSpaceBeforeKeywords: ['while'] }); + + assert(checker.checkString( + 'while (x < 5) {\n' + + 'x++;\n' + + '}' + ).isEmpty()); + }); + + it('should show different error if there is more than one space', function() { + checker.configure({ requireSpaceBeforeKeywords: ['else'] }); + + var errors = checker.checkString('if (true) {\n} else { x++; }'); + var error = errors.getErrorList()[0]; + + assert(errors.explainError(error).indexOf('Should be one space instead of 2, before "else"') === 0); + }); + + it('should not trigger error for comments', function() { + checker.configure({ requireSpaceBeforeKeywords: ['else'] }); + assert(checker.checkString('if (true) {\n} /**/ else { x++; }').isEmpty()); + }); + + it('should trigger different error for comments with more than one space', function() { + checker.configure({ requireSpaceBeforeKeywords: ['else'] }); + + var errors = checker.checkString('if (true) {\n} /**/ else { x++; }'); + var error = errors.getErrorList()[0]; + + assert(errors.explainError(error).indexOf('Should be one space instead of 2, before "else"') === 0); + }); + + it('should report on all possible ES3 keywords if a value of true is supplied', function() { + checker.configure({ requireSpaceBeforeKeywords: true }); + + var errors = checker.checkString('if (true) {\n}else { x++; }'); + var error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Missing space before "else" keyword') === 0); + + errors = checker.checkString('/**/if (true) {\n}else { x++; }'); + error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Missing space before "else" keyword') === 0); + + errors = checker.checkString('do {\nx++;\n}while (x < 5)'); + error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Missing space before "while" keyword') === 0); + + errors = checker.checkString('try {\nx++;\n}catch (e) {}'); + error = errors.getErrorList()[0]; + assert(errors.getErrorCount() === 1); + assert(errors.explainError(error).indexOf('Missing space before "catch" keyword') === 0); + }); +}); diff --git a/test/rules/validate-indentation.js b/test/rules/validate-indentation.js index 5e8e3bc91..e407cf32d 100644 --- a/test/rules/validate-indentation.js +++ b/test/rules/validate-indentation.js @@ -104,81 +104,100 @@ describe('rules/validate-indentation', function() { ]); }); - it('should report errors for indent after no indent in same switch statement', function() { - checker.configure({ validateIndentation: 4 }); - assert( - checker.checkString( - 'switch(value){\n' + - ' case "1":\n' + - ' break;\n' + - ' case "2":\n' + - ' break;\n' + - ' default:\n' + - ' break;\n' + - '}' - ).getErrorCount() === 1 - ); - }); - - it('should report errors for no indent after indent in same switch statement', function() { - checker.configure({ validateIndentation: 4 }); - assert( - checker.checkString( - 'switch(value){\n' + - ' case "1":\n' + - ' break;\n' + - ' case "2":\n' + - ' break;\n' + - ' default:\n' + - ' break;\n' + - '}' - ).getErrorCount() === 1 - ); - }); + describe('switch identation', function() { + beforeEach(function() { + checker.configure({ validateIndentation: 4 }); + }); - it('should report errors for no indent after indent in different switch statements', function() { - checker.configure({ validateIndentation: 4 }); - assert( - checker.checkString( - 'switch(value){\n' + - ' case "1":\n' + - ' case "2":\n' + - ' break;\n' + - ' default:\n' + - ' break;\n' + + it('should not report errors for indent when return statement is used instead of break', function() { + assert( + checker.checkString( + 'function foo() {\n' + + ' var a = "a";\n' + + ' switch(a) {\n' + + ' case "a":\n' + + ' return "A";\n' + + ' case "b":\n' + + ' return "B";\n' + + ' }\n' + '}\n' + + 'foo();' + ).getErrorCount() === 0 + ); + }); + + it('should report errors for indent after no indent in same switch statement', function() { + assert( + checker.checkString( 'switch(value){\n' + - ' case "1":\n' + - ' break;\n' + - ' case "2":\n' + - ' break;\n' + - ' default:\n' + - ' break;\n' + - '}' - ).getErrorCount() === 3 - ); - }); + ' case "1":\n' + + ' break;\n' + + ' case "2":\n' + + ' break;\n' + + ' default:\n' + + ' break;\n' + + '}' + ).getErrorCount() === 1 + ); + }); - it('should report errors for indent after no indent in different switch statements', function() { - checker.configure({ validateIndentation: 4 }); - assert( - checker.checkString( - 'switch(value){\n' + - ' case "1":\n' + - ' case "2":\n' + - ' break;\n' + - ' default:\n' + - ' break;\n' + - '}\n' + + it('should report errors for no indent after indent in same switch statement', function() { + assert( + checker.checkString( + 'switch(value){\n' + + ' case "1":\n' + + ' break;\n' + + ' case "2":\n' + + ' break;\n' + + ' default:\n' + + ' break;\n' + + '}' + ).getErrorCount() === 1 + ); + }); + + it('should report errors for no indent after indent in different switch statements', function() { + assert( + checker.checkString( 'switch(value){\n' + - ' case "1":\n' + - ' break;\n' + - ' case "2":\n' + - ' break;\n' + - ' default:\n' + - ' break;\n' + - '}' - ).getErrorCount() === 3 - ); + ' case "1":\n' + + ' case "2":\n' + + ' break;\n' + + ' default:\n' + + ' break;\n' + + '}\n' + + 'switch(value){\n' + + ' case "1":\n' + + ' break;\n' + + ' case "2":\n' + + ' break;\n' + + ' default:\n' + + ' break;\n' + + '}' + ).getErrorCount() === 3 + ); + }); + + it('should report errors for indent after no indent in different switch statements', function() { + assert( + checker.checkString( + 'switch(value){\n' + + ' case "1":\n' + + ' case "2":\n' + + ' break;\n' + + ' default:\n' + + ' break;\n' + + '}\n' + + 'switch(value){\n' + + ' case "1":\n' + + ' break;\n' + + ' case "2":\n' + + ' break;\n' + + ' default:\n' + + ' break;\n' + + '}' + ).getErrorCount() === 3 + ); + }); }); }); diff --git a/test/test.validate-parameter-separator.js b/test/rules/validate-parameter-separator.js similarity index 61% rename from test/test.validate-parameter-separator.js rename to test/rules/validate-parameter-separator.js index 48af9a5ac..260df6852 100644 --- a/test/test.validate-parameter-separator.js +++ b/test/rules/validate-parameter-separator.js @@ -1,4 +1,4 @@ -var Checker = require('../lib/checker'); +var Checker = require('../../lib/checker'); var assert = require('assert'); describe('rules/validate-parameter-separator', function() { @@ -16,6 +16,7 @@ describe('rules/validate-parameter-separator', function() { ', ', ' , ', ]; + validSeparators.forEach(function(sep) { assert.doesNotThrow(function() { checker.configure({ validateParameterSeparator: sep }); @@ -31,6 +32,7 @@ describe('rules/validate-parameter-separator', function() { ' ,', ', ', ]; + invalidSeparators.forEach(function(sep) { assert.throws(function() { checker.configure({ validateParameterSeparator: sep }); @@ -39,86 +41,158 @@ describe('rules/validate-parameter-separator', function() { }); describe('(comma)', function() { - it('should report unexpected space for function a(b, c) {}', function() { + beforeEach(function() { checker.configure({ validateParameterSeparator: ',' }); + }); + + it('should report unexpected space for function a(b, c) {}', function() { assert.strictEqual(checker.checkString('function a(b, c) {}').getErrorCount(), 1); }); + it('should report unexpected space for function a(b ,c) {}', function() { - checker.configure({ validateParameterSeparator: ',' }); assert.strictEqual(checker.checkString('function a(b ,c) {}').getErrorCount(), 1); }); + it('should report 2 unexpected spaces for function a(b , c) {}', function() { - checker.configure({ validateParameterSeparator: ',' }); assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); }); + it('should not report any errors for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ',' }); assert.strictEqual(checker.checkString('function a(b,c) {}').getErrorCount(), 0); }); + it('should not report any errors for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ',' }); assert.strictEqual(checker.checkString('function a(b,\nc) {}').getErrorCount(), 0); }); + it('should not report any errors for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ',' }); assert.strictEqual(checker.checkString('function a(b\n,c) {}').getErrorCount(), 0); }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b ,c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b, c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); + }); + }); describe('(comma space)', function() { - it('should report unexpected space for function a(b , c) {}', function() { + beforeEach(function() { checker.configure({ validateParameterSeparator: ', ' }); + }); + + it('should report unexpected space for function a(b , c) {}', function() { assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 1); }); + it('should report missing space for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ', ' }); assert.strictEqual(checker.checkString('function a(b,c) {}').getErrorCount(), 1); }); + it('should not report any errors for function a(b, c) {}', function() { - checker.configure({ validateParameterSeparator: ', ' }); assert.strictEqual(checker.checkString('function a(b, c) {}').getErrorCount(), 0); }); + it('should not report any errors for function a(b, c) {}', function() { - checker.configure({ validateParameterSeparator: ', ' }); assert.strictEqual(checker.checkString('function a(b\n, c) {}').getErrorCount(), 0); }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b, c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b, c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); + }); + }); describe('(space comma)', function() { - it('should report unexpected space for function a(b , c) {}', function() { + beforeEach(function() { checker.configure({ validateParameterSeparator: ' ,' }); + }); + + it('should report unexpected space for function a(b , c) {}', function() { assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 1); }); + it('should report missing space for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ' ,' }); assert.strictEqual(checker.checkString('function a(b,c) {}').getErrorCount(), 1); }); + it('should not report any errors for function a(b ,c) {}', function() { - checker.configure({ validateParameterSeparator: ' ,' }); assert.strictEqual(checker.checkString('function a(b ,c) {}').getErrorCount(), 0); }); + it('should not report any errors for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ' ,' }); assert.strictEqual(checker.checkString('function a(b ,\nc) {}').getErrorCount(), 0); }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b ,c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b ,c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); + }); + }); describe('(space comma space)', function() { - it('should report missing space for function a(b, c) {}', function() { + beforeEach(function() { checker.configure({ validateParameterSeparator: ' , ' }); + }); + + it('should report missing space for function a(b, c) {}', function() { assert.strictEqual(checker.checkString('function a(b, c) {}').getErrorCount(), 1); }); + it('should report missing space for function a(b ,c) {}', function() { - checker.configure({ validateParameterSeparator: ' , ' }); assert.strictEqual(checker.checkString('function a(b ,c) {}').getErrorCount(), 1); }); + it('should report 2 missing spaces for function a(b,c) {}', function() { - checker.configure({ validateParameterSeparator: ' , ' }); assert.strictEqual(checker.checkString('function a(b,c) {}').getErrorCount(), 2); }); + it('should not report any errors for function a(b , c) {}', function() { - checker.configure({ validateParameterSeparator: ' , ' }); assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 0); }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 1); + }); + + it('should report errors for function a(b,c) {}', function() { + assert.strictEqual(checker.checkString('function a(b , c) {}').getErrorCount(), 2); + }); + }); }); diff --git a/test/string-checker.js b/test/string-checker.js index 62e0cbc37..919285f60 100644 --- a/test/string-checker.js +++ b/test/string-checker.js @@ -66,7 +66,7 @@ describe('modules/string-checker', function() { assert(false); } catch (e) { - assert(e.toString() === 'Error: Preset "not-exist" does not exist'); + assert.equal(e.toString(), 'AssertionError: Preset "not-exist" does not exist'); } }); @@ -124,5 +124,81 @@ describe('modules/string-checker', function() { assert(errors.length === 1); assert(errors2.length === 0); }); + + it('should not be used when not a number', function() { + var errors; + checker.configure({ + requireSpaceBeforeBinaryOperators: ['='], + maxErrors: NaN + }); + + errors = checker.checkString('var foo=1;\n var bar=2;').getErrorList(); + assert(errors.length > 0); + }); + }); + + describe('esprima version', function() { + var customDescription = 'in no way a real error message'; + var customEsprima = { + parse: function() { + var error = new Error(); + error.description = customDescription; + + throw error; + } + }; + + it('uses a custom esprima when provided to the constructor', function() { + checker = new Checker({ esprima: customEsprima }); + checker.registerDefaultRules(); + + var errors = checker.checkString('import { foo } from "bar";'); + var error = errors.getErrorList()[0]; + + assert(error.rule === 'parseError'); + assert(error.message === customDescription); + }); + + it('uses a custom esprima when both esprima and esnext are provided to the constructor', function() { + checker = new Checker({ esprima: customEsprima, esnext: true }); + checker.registerDefaultRules(); + + var errors = checker.checkString('import { foo } from "bar";'); + var error = errors.getErrorList()[0]; + + assert(error.rule === 'parseError'); + assert(error.message === customDescription); + }); + + it('uses the harmony esprima when true is provided to the constructor', function() { + checker = new Checker({ esnext: true }); + checker.registerDefaultRules(); + + var errors = checker.checkString('import { foo } from "bar";'); + assert(errors.isEmpty()); + }); + + it('uses the harmony esprima when esnext is set to true in the config', function() { + checker = new Checker(); + checker.registerDefaultRules(); + checker.configure({ esnext: true }); + + var errors = checker.checkString('import { foo } from "bar";'); + // Make sure that multiple checks don't fail + var errors2 = checker.checkString('import { bar } from "foo";'); + assert(errors.isEmpty()); + assert(errors2.isEmpty()); + }); + + it('uses the default esprima when falsely or no argument is provided to the constructor', function() { + checker = new Checker(); + checker.registerDefaultRules(); + + var errors = checker.checkString('import { foo } from "bar";'); + var error = errors.getErrorList()[0]; + + assert(error.rule === 'parseError'); + assert(error.message !== customDescription); + }); }); });