diff --git a/.gitignore b/.gitignore index d2d8fcb3..5bfac4ea 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ node_modules # Users Environment Variables .lock-wscript + +.vscode diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f31702e4..8c1069e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ By making a contribution to this project, I certify that: ## Emoji Cheatsheet -When creating creating commits or updating the CHANGELOG, please **start** the commit message or update with one of the following applicable Emoji. Emoji should not be used at the start of issue or pull request titles. +When creating commits or updating the CHANGELOG, please **start** the commit message or update with one of the following applicable Emoji. Emoji should not be used at the start of issue or pull request titles. * :art: `:art:` when improving the format/structure of the code * :racehorse: `:racehorse:` when improving performance diff --git a/docs/rules/no-excessive-empty-lines.md b/docs/rules/no-excessive-empty-lines.md new file mode 100644 index 00000000..195e00ba --- /dev/null +++ b/docs/rules/no-excessive-empty-lines.md @@ -0,0 +1,47 @@ +# No Excessive Empty Lines + +Rule `no-excessive-empty-lines` will disallow excessive amounts of empty lines. + +- Consecutive blank lines are not allowed +- Blank lines at the start of a block are not allowed +- Blank lines at the end of a block are not allowed + +## Options + +* `allow-consecutive`: `true`/`false` (defaults to `false`) +* `allow-at-block-end`: `true`/`false` (defaults to `false`) +* `allow-at-block-start`: `true`/`false` (defaults to `false`)`) + +## Examples + +When enabled the following are allowed: + +```scss +.foo { + content: 'bar'; + content: 'baz'; + + .waldo { + content: 'where'; + } +} +``` + +When enabled the following are disallowed: + +```scss + + +.foo { + content: 'bar'; + content: 'baz' + + + .waldo { + content: 'where' + + } +} + + +``` diff --git a/index.js b/index.js index 8bd498f7..f4d551b7 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,8 @@ sassLint.failOnError = function (results, options, configPath) { configOptions = this.getConfig(options, configPath).options; if (errorCount.count > 0) { - throw new exceptions.SassLintFailureError(errorCount.count + ' errors were detected in \n- ' + errorCount.files.join('\n- ')); + const pluralized = errorCount.count === 1 ? 'error was' : 'errors were'; + throw new exceptions.SassLintFailureError(errorCount.count + ' ' + pluralized + ' detected in \n- ' + errorCount.files.join('\n- ')); } if (!isNaN(configOptions['max-warnings']) && warningCount.count > configOptions['max-warnings']) { diff --git a/lib/rules/no-excessive-empty-lines.js b/lib/rules/no-excessive-empty-lines.js new file mode 100644 index 00000000..fa1f3c00 --- /dev/null +++ b/lib/rules/no-excessive-empty-lines.js @@ -0,0 +1,127 @@ +'use strict'; + +var helpers = require('../helpers'); + +const countOccurences = function (str, substr) { + return str.split(substr).length - 1; +}; + +const checkNodeForEmptyness = function (node) { + if (node.type !== 'space') { + return false; + } + + return countOccurences(node.content, '\n') > 1; +}; + +const addError = function (result, parser, message, line) { + return helpers.addUnique(result, { + 'ruleId': parser.rule.name, + 'line': line, + 'column': 1, + message, + 'severity': parser.severity + }); +}; + +const multipleConsecutiveError = function (result, parser, line) { + return addError(result, parser, 'Multiple consecutive empty lines not allowed', line); +}; + +const startOfBlockError = function (result, parser, line) { + return addError(result, parser, 'Empty lines at start of block not allowed', line); +}; + +const endOfBlockError = function (result, parser, line) { + return addError(result, parser, 'Empty lines at end of block not allowed', line); +}; + +module.exports = { + 'name': 'no-excessive-empty-lines', + 'defaults': { + 'allow-consecutive': false, + 'allow-at-block-end': false, + 'allow-at-block-start': false, + }, + 'detect': function (ast, parser) { + let result = []; + + if (!parser.options['allow-consecutive']) { + const source = ast.toString(); + const re = /^\n\n+/gm; + let m = null; + do { + m = re.exec(source); + if (m) { + const lineNumber = source + .substr(0, m.index) + .split('\n').length; + + result = multipleConsecutiveError(result, parser, lineNumber); + } + } while (m); + } + + const forbidAtStart = !parser.options['allow-at-block-start']; + const forbidAtEnd = !parser.options['allow-at-block-end']; + + if (forbidAtStart || forbidAtEnd) { + ast.traverseByType('block', function (node) { + if (!Array.isArray(node.content) || node.content.length < 1) { + return true; + } + + if (ast.syntax === 'scss') { + if (forbidAtStart) { + if (checkNodeForEmptyness(node.content[0])) { + result = startOfBlockError(result, parser, node.start.line + 1); + } + } + + if (forbidAtEnd) { + const n = node.content[node.content.length - 1]; + if (checkNodeForEmptyness(n)) { + result = endOfBlockError(result, parser, n.start.line + 1); + } + } + } + else if (ast.syntax === 'sass') { + if (forbidAtStart) { + if (node.content[0].type === 'space' && + countOccurences(node.content[0].content, '\n')) { + result = startOfBlockError(result, parser, node.start.line); + } + } + } + + return true; + }); + } + + if (!parser.options['allow-at-block-end']) { + ast.traverseByType('block', function (node) { + if (!Array.isArray(node.content) || node.content.length < 1) { + return true; + } + + if (node.content[0].type !== 'space') { + return true; + } + + if (countOccurences(node.content[0].content, '\n') > 1) { + result = helpers.addUnique(result, { + 'ruleId': parser.rule.name, + 'line': node.start.line + 1, + 'column': 1, + 'message': 'Empty lines at end of block not allowed', + 'severity': parser.severity + }); + } + + return true; + }); + } + + return result; + } +}; diff --git a/tests/rules/no-excessive-empty-lines.js b/tests/rules/no-excessive-empty-lines.js new file mode 100644 index 00000000..acc6a054 --- /dev/null +++ b/tests/rules/no-excessive-empty-lines.js @@ -0,0 +1,35 @@ +'use strict'; + +var lint = require('./_lint'); + +////////////////////////////// +// SCSS syntax tests +////////////////////////////// +describe('no excessive empty lines - scss', function () { + var file = lint.file('no-excessive-empty-lines.scss'); + + it('enforce', function (done) { + lint.test(file, { + 'no-excessive-empty-lines': 1 + }, function (data) { + lint.assert.equal(15, data.warningCount); + done(); + }); + }); +}); + +////////////////////////////// +// Sass syntax tests +////////////////////////////// +describe('no excessive empty lines - sass', function () { + var file = lint.file('no-excessive-empty-lines.sass'); + + it('enforce', function (done) { + lint.test(file, { + 'no-excessive-empty-lines': 1 + }, function (data) { + lint.assert.equal(15, data.warningCount); + done(); + }); + }); +}); diff --git a/tests/sass/no-excessive-empty-lines.sass b/tests/sass/no-excessive-empty-lines.sass new file mode 100644 index 00000000..da949055 --- /dev/null +++ b/tests/sass/no-excessive-empty-lines.sass @@ -0,0 +1,93 @@ +.foo + + content: 'foo' + + + +.foo + content: 'foo' +.bar + content: 'bar' + +.foo + @include bar + +.foo + @include bar($qux) + content: 'foo' + + +.foo + @include bar($qux) + content: 'foo' + + +.foo + + @include bar($qux) + content: 'foo' + + +.foo + @include bar($qux) + &:after + content: 'foo' + + + +.foo + + &__bar + content: 'foo' + + + content: 'foo' + +.foo + content: 'foo' + + &__bar + content: 'foo' + + +.foo + content: 'foo' + + &__bar + content: 'foo' + + + +.foo + &:before + content: 'foo' + + +.foo + content: 'foo' + + &::first-line + content: 'foo' + + +h1 + content: 'foo' +h2 + content: 'foo' +h3 + content: 'foo' +h4 + content: 'foo' + + +.foo + .bar + content: 'foo' +.foo + .bar, +h2 + content: 'foo' + + +[type=text] + content: 'foo' +h1.foo + content: 'foo' diff --git a/tests/sass/no-excessive-empty-lines.scss b/tests/sass/no-excessive-empty-lines.scss new file mode 100644 index 00000000..dba49ea9 --- /dev/null +++ b/tests/sass/no-excessive-empty-lines.scss @@ -0,0 +1,118 @@ +.foo { + + content: 'foo'; + + +} + +.foo { + content: 'foo'; +} +.bar { + content: 'bar'; +} + +.foo { + @include bar; +} + +.foo { + @include bar($qux) { + content: 'foo'; + } +} + +.foo { + @include bar($qux) { + content: 'foo'; + } +} + +.foo { + + @include bar($qux) { + content: 'foo'; + } +} + +.foo { + @include bar($qux) { + &:after { + content: 'foo'; + } + } + + +} + +.foo { + + &__bar { + content: 'foo'; + } + + content: 'foo'; +} + +.foo { + content: 'foo'; + + &__bar { + content: 'foo'; + } +} + +.foo { + content: 'foo'; + + &__bar { + content: 'foo'; + } +} + + +.foo { + &:before { + content: 'foo'; + } +} + +.foo { + content: 'foo'; + + &::first-line { + content: 'foo'; + } +} + + +h1 { content: 'foo'; } +h2 { content: 'foo'; } +h3 { content: 'foo'; } +h4 { content: 'foo'; } + + +h1 { content: 'foo'; } + +h2 { content: 'foo'; } + +h3 { content: 'foo'; } + +h4 { content: 'foo'; } + + +.foo + .bar { + content: 'foo'; +} +.foo + .bar, +h2 { + content: 'foo'; +} + + +[type=text] { + content: 'foo'; +} +h1.foo { + content: 'foo'; +}