diff --git a/CHANGELOG.md b/CHANGELOG.md index ee6c74a5..e0c23689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Sass Lint Changelog +## v1.9.1 + +**August 25, 2016** + +**Fixes** +* Fixed an issue with nth selectors in the `no-mergeable-selectors` rule [#834](https://github.com/sasstools/sass-lint/issues/834) +* Fixed an issue with atrule arguments containing functions in the `no-mergeable-selectors` rule [#826](https://github.com/sasstools/sass-lint/issues/826) +* Fixed an issue with hex colors being ignored in the `shorthand-values` rule [#836](https://github.com/sasstools/sass-lint/pull/836) + +## v1.9.0 + +**August 18, 2016** + +**Fixes** +* Fixed an issue with the indentation rule when it encountered at-rules with no block immediately preceding a map [#779](https://github.com/sasstools/sass-lint/issues/779) [#783](https://github.com/sasstools/sass-lint/issues/783) +* Fixed an issue in `single-lint-per-selector` where inline comments were seen as selectors [#789](https://github.com/sasstools/sass-lint/issues/789) +* Fixed an issue with interpolation in placeholders within the `bem-depth` rule [#782](https://github.com/sasstools/sass-lint/issues/782) +* Removed duplicated code from `no-mergeable-selectors` to helper methods + +**Documentation** +* Fixed typos in no-vendor-prefixes rule documentation [#787](https://github.com/sasstools/sass-lint/issues/787) +* Added link to Visual Studio extension [#815](https://github.com/sasstools/sass-lint/pull/815) + +**New Rules** +* Added the `no-color-hex` rule to disallow all hexadecimal colour definitions [#754](https://github.com/sasstools/sass-lint/issues/754) + +**Updates** +* Gonzales-pe updated to version 3.4.4 which fixes a lot of longstanding issues see the [Changelog](https://github.com/tonyganch/gonzales-pe/blob/dev/CHANGELOG.md) + ## v1.8.2 **June 23, 2016** diff --git a/README.md b/README.md index 555a4a75..aab7434a 100644 --- a/README.md +++ b/README.md @@ -194,3 +194,5 @@ Our AST is [Gonzales-PE](https://github.com/tonyganch/gonzales-pe/tree/dev). Eac * [Sublime Text](https://github.com/skovhus/SublimeLinter-contrib-sass-lint) * [Brackets](https://github.com/petetnt/brackets-sass-lint) * [IntelliJ IDEA, RubyMine, WebStorm, PhpStorm, PyCharm](https://github.com/idok/sass-lint-plugin) +* [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=glen-84.sass-lint) +* [Vim](https://github.com/gcorne/vim-sass-lint) diff --git a/bin/sass-lint.js b/bin/sass-lint.js index 787e9330..a6218b8f 100755 --- a/bin/sass-lint.js +++ b/bin/sass-lint.js @@ -6,15 +6,7 @@ var program = require('commander'), lint = require('../index'); var configPath, - ignores, - configOptions = {}, - exitCode = 0; - -var tooManyWarnings = function (detects) { - var warningCount = lint.warningCount(detects).count; - - return warningCount > 0 && warningCount > program.maxWarnings; -}; + configOptions = {}; var detectPattern = function (pattern) { var detects; @@ -25,13 +17,7 @@ var detectPattern = function (pattern) { lint.outputResults(detects, configOptions, configPath); } - if (lint.errorCount(detects).count || tooManyWarnings(detects)) { - exitCode = 1; - } - - if (program.exit) { - lint.failOnError(detects); - } + lint.failOnError(detects, configOptions, configPath); }; program @@ -47,21 +33,16 @@ program .option('--max-warnings [integer]', 'Number of warnings to trigger nonzero exit code') .parse(process.argv); +// Create "options" and "files" dictionaries if they don't exist +configOptions.files = configOptions.files || {}; +configOptions.options = configOptions.options || {}; if (program.config && program.config !== true) { configPath = program.config; } if (program.ignore && program.ignore !== true) { - ignores = program.ignore.split(', '); - if (configOptions.hasOwnProperty('files')) { - configOptions.files.ignore = ignores; - } - else { - configOptions.files = { - 'ignore': ignores - }; - } + configOptions.files.ignore = program.ignore.split(', '); } if (program.syntax && ['sass', 'scss'].indexOf(program.syntax) > -1) { @@ -69,25 +50,15 @@ if (program.syntax && ['sass', 'scss'].indexOf(program.syntax) > -1) { } if (program.format && program.format !== true) { - if (configOptions.hasOwnProperty('options')) { - configOptions.options.formatter = program.format; - } - else { - configOptions.options = { - 'formatter': program.format - }; - } + configOptions.options.formatter = program.format; } if (program.output && program.output !== true) { - if (configOptions.hasOwnProperty('options')) { - configOptions.options['output-file'] = program.output; - } - else { - configOptions.options = { - 'output-file': program.output - }; - } + configOptions.options['output-file'] = program.output; +} + +if (program.maxWarnings && program.maxWarnings !== true) { + configOptions.options['max-warnings'] = program.maxWarnings; } if (program.args.length === 0) { @@ -98,7 +69,3 @@ else { detectPattern(path); }); } - -process.on('exit', function () { - process.exit(exitCode); // eslint-disable-line no-process-exit -}); diff --git a/docs/options/max-warnings.md b/docs/options/max-warnings.md new file mode 100644 index 00000000..32d6e8ed --- /dev/null +++ b/docs/options/max-warnings.md @@ -0,0 +1,29 @@ +# Max warnings + +An error will be thrown if the total number of warnings exceeds the `max-warnings` setting. + +## Examples + +This can be set as a command-line option: + +``` bash +$ sass-lint --max-warnings 50 +``` + +In `.sass-lint.yml`: + +``` yaml +options: + max-warnings: 50 +``` + +Or inside a script: + +``` javascript +var sassLint = require('sass-lint'), + config = {options: {'max-warnings': 50}}; + +results = sassLint.lintFiles('sass/**/*.scss', config) +sassLint.failOnError(results, config); +``` + diff --git a/docs/rules/no-color-hex.md b/docs/rules/no-color-hex.md new file mode 100644 index 00000000..9d55d95e --- /dev/null +++ b/docs/rules/no-color-hex.md @@ -0,0 +1,33 @@ +# No Color Hex + +Rule `no-color-hex` will disallow the use of hexadecimal colors + +## Examples + +When enabled the following are disallowed. + +```scss +$foo-color: #456; + +.bar { + background: linear-gradient(top, #3ff, #ddd); +} + +.baz { + color: #fff; +} +``` + +When enabled the following are allowed: + +```scss +$foo-color: red; + +.bar { + background: linear-gradient(top, blue, green); +} + +.baz { + color: white; +} +``` diff --git a/docs/rules/no-url-domains.md b/docs/rules/no-url-domains.md new file mode 100644 index 00000000..8c536b1a --- /dev/null +++ b/docs/rules/no-url-domains.md @@ -0,0 +1,37 @@ +# No Url Domains + +Rule `no-url-domains` will enforce that domains are not used within urls. + +## Examples + +When enabled, the following are allowed: + +```scss +.foo { + background-image: url('/img/bar.png'); +} + +.foo { + background-image: url('img/bar.png'); +} + +.foo { + background-image: url('bar.png'); +} +``` + +When enabled, the following are disallowed: + +```scss +.foo { + background-image: url('https://foo.com/img/bar.png'); +} + +.foo { + background-image: url('http://foo.com/img/bar.png'); +} + +.foo { + background-image: url('//foo.com/img/bar.png'); +} +``` diff --git a/docs/rules/no-url-protocols.md b/docs/rules/no-url-protocols.md index ebf620b3..50baa5ff 100644 --- a/docs/rules/no-url-protocols.md +++ b/docs/rules/no-url-protocols.md @@ -2,9 +2,17 @@ Rule `no-url-protocols` will enforce that protocols and domains are not used within urls. +## Options + +* `allow-protocol-relative-urls`: `true`/`false` (defaults to `false`) +> This option is scheduled to be deprecated in favour of the [no-url-domains](https://github.com/sasstools/sass-lint/blob/develop/docs/rules/no-url-domains.md) rule in sass-lint 2.0. + ## Examples -When enabled, the following are allowed: +### `allow-protocol-relative-urls` + + +When `allow-protocol-relative-urls: false`, the following are allowed: ```scss .foo { @@ -20,7 +28,7 @@ When enabled, the following are allowed: } ``` -When enabled, the following are disallowed: +When `allow-protocol-relative-urls: false`, the following are disallowed: ```scss .foo { @@ -35,3 +43,35 @@ When enabled, the following are disallowed: background-image: url('//foo.com/img/bar.png'); } ``` + +When `allow-protocol-relative-urls: true`, the following are allowed: + +```scss +.foo { + background-image: url('//foo.com/img/bar.png'); +} + +.foo { + background-image: url('/img/bar.png'); +} + +.foo { + background-image: url('img/bar.png'); +} + +.foo { + background-image: url('bar.png'); +} +``` + +When `allow-protocol-relative-urls: true`, the following are disallowed: + +```scss +.foo { + background-image: url('https://foo.com/img/bar.png'); +} + +.foo { + background-image: url('http://foo.com/img/bar.png'); +} +``` diff --git a/docs/sass-lint.yml b/docs/sass-lint.yml index 3de8a760..682ec6c5 100644 --- a/docs/sass-lint.yml +++ b/docs/sass-lint.yml @@ -9,6 +9,8 @@ options: formatter: html # Output file instead of logging results output-file: 'linters/sass-lint.html' + # Raise an error if more than 50 warnings are generated + max-warnings: 50 # File Options files: include: 'sass/**/*.s+(a|c)ss' diff --git a/index.js b/index.js index decbc923..e222c616 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ var slConfig = require('./lib/config'), groot = require('./lib/groot'), + exceptions = require('./lib/exceptions'), helpers = require('./lib/helpers'), slRules = require('./lib/rules'), glob = require('glob'), @@ -284,14 +285,30 @@ sassLint.outputResults = function (results, options, configPath) { * Throws an error if there are any errors detected. The error includes a count of all errors * and a list of all files that include errors. * - * @param {object} results our results object + * @param {object} results - our results object + * @param {object} [options] - extra options to use when running failOnError, e.g. max-warnings + * @param {string} [configPath] - path to the config file * @returns {void} */ -sassLint.failOnError = function (results) { - var errorCount = this.errorCount(results); +sassLint.failOnError = function (results, options, configPath) { + // Default parameters + options = typeof options !== 'undefined' ? options : {}; + configPath = typeof configPath !== 'undefined' ? configPath : null; + + var errorCount = this.errorCount(results), + warningCount = this.warningCount(results), + configOptions = this.getConfig(options, configPath).options; if (errorCount.count > 0) { - throw new Error(errorCount.count + ' errors were detected in \n- ' + errorCount.files.join('\n- ')); + throw new exceptions.SassLintFailureError(errorCount.count + ' errors were detected in \n- ' + errorCount.files.join('\n- ')); + } + + if (!isNaN(configOptions['max-warnings']) && warningCount.count > configOptions['max-warnings']) { + throw new exceptions.MaxWarningsExceededError( + 'Number of warnings (' + warningCount.count + + ') exceeds the allowed maximum of ' + configOptions['max-warnings'] + + '.\n' + ); } }; diff --git a/lib/config/sass-lint.yml b/lib/config/sass-lint.yml index e98c3db9..4a56ea9e 100644 --- a/lib/config/sass-lint.yml +++ b/lib/config/sass-lint.yml @@ -18,6 +18,7 @@ rules: # Disallows no-attribute-selectors: 0 + no-color-hex: 0 no-color-keywords: 1 no-color-literals: 1 no-combinators: 0 @@ -37,6 +38,7 @@ rules: no-trailing-zero: 1 no-transition-all: 1 no-universal-selectors: 0 + no-url-domains: 1 no-url-protocols: 1 no-vendor-prefixes: 1 no-warn: 1 diff --git a/lib/exceptions.js b/lib/exceptions.js new file mode 100644 index 00000000..77e15683 --- /dev/null +++ b/lib/exceptions.js @@ -0,0 +1,19 @@ +'use strict'; + +var util = require('util'); + +module.exports = { + SassLintFailureError: function (message) { + Error.captureStackTrace(this, this.constructor); + this.name = 'SassLintFailureError'; + this.message = message; + }, + MaxWarningsExceededError: function (message) { + Error.captureStackTrace(this, this.constructor); + this.name = 'MaxWarningsExceededError'; + this.message = message; + } +}; + +util.inherits(module.exports.SassLintFailureError, Error); +util.inherits(module.exports.MaxWarningsExceededError, Error); diff --git a/lib/rules/bem-depth.js b/lib/rules/bem-depth.js index 9910b765..356efa25 100644 --- a/lib/rules/bem-depth.js +++ b/lib/rules/bem-depth.js @@ -1,6 +1,7 @@ 'use strict'; var helpers = require('../helpers'); +var selectorHelpers = require('../selector-helpers'); /** * Get number of BEM elements in @@ -32,18 +33,19 @@ module.exports = { maxDepth = parser.options['max-depth']; if (node.is('placeholder')) { - name = node.first('ident') && node.first('ident').content; - depth = bemDepth(name); - - if (name && depth > maxDepth) { - result = helpers.addUnique(result, { - 'ruleId': parser.rule.name, - 'line': node.start.line, - 'column': node.start.column, - 'message': ['Placeholder \'%', name, '\' should have ', maxDepth, ' or fewer BEM elements, but ', - depth, ' were found.'].join(''), - 'severity': parser.severity - }); + name = selectorHelpers.constructSelector(node); + if (name) { + depth = bemDepth(name); + if (depth > maxDepth) { + result = helpers.addUnique(result, { + 'ruleId': parser.rule.name, + 'line': node.start.line, + 'column': node.start.column, + 'message': ['Placeholder \'%', name, '\' should have ', maxDepth, ' or fewer BEM elements, but ', + depth, ' were found.'].join(''), + 'severity': parser.severity + }); + } } } else { diff --git a/lib/rules/indentation.js b/lib/rules/indentation.js index 7ef79b16..83b688b1 100644 --- a/lib/rules/indentation.js +++ b/lib/rules/indentation.js @@ -166,7 +166,7 @@ module.exports = { mixedWarning = false; } // if we're in an atrule make we need to possibly handle multiline arguments - if (n.is('atrule')) { + if (n.is('atrule') && n.contains('block')) { inAtRule = true; inBlock = false; } diff --git a/lib/rules/no-color-hex.js b/lib/rules/no-color-hex.js new file mode 100644 index 00000000..d5190056 --- /dev/null +++ b/lib/rules/no-color-hex.js @@ -0,0 +1,22 @@ +'use strict'; + +var helpers = require('../helpers'); + +module.exports = { + 'name': 'no-color-hex', + 'defaults': {}, + 'detect': function (ast, parser) { + var result = []; + + ast.traverseByType('color', function (value) { + result = helpers.addUnique(result, { + 'ruleId': parser.rule.name, + 'line': value.start.line, + 'column': value.start.column, + 'message': 'Hexadecimal colors should not be used', + 'severity': parser.severity + }); + }); + return result; + } +}; diff --git a/lib/rules/no-mergeable-selectors.js b/lib/rules/no-mergeable-selectors.js index 9bb70d48..d5752e06 100644 --- a/lib/rules/no-mergeable-selectors.js +++ b/lib/rules/no-mergeable-selectors.js @@ -1,10 +1,10 @@ 'use strict'; -var helpers = require('../helpers'); +var helpers = require('../helpers'), + selectorHelpers = require('../selector-helpers'); var mergeableNodes = ['atrule', 'include', 'ruleset'], validAtRules = ['media'], - simpleIdents = ['ident', 'number', 'operator', 'combinator', 'string', 'parentSelector', 'delimiter', 'typeSelector', 'attributeMatch'], curLevel = 0, curSelector = [], parentSelector = [], @@ -12,109 +12,6 @@ var mergeableNodes = ['atrule', 'include', 'ruleset'], syntax = ''; -/** - * Adds grammar around our content blocks to construct selectors with - * more readable formats. - * - * @param {object} val - The current value node - * @param {string} prefix - The grammar to prefix the value with - * @param {string} suffix - The grammar to add after the value - * @returns {string} The correct readable format - */ -var addGrammar = function (val, prefix, suffix) { - return prefix + val.content + suffix; -}; - -/** - * Adds grammar around our content blocks to construct selectors with - * more readable formats and loops the content as they're within sub blocks. - * - * @param {object} val - The current value node - * @param {string} prefix - The grammar to prefix the value with - * @param {string} suffix - The grammar to add after the value - * @param {function} constructSelector - The callback we wish to use which means constructSelector in this instance - * @returns {string} The correct readable format - */ -var constructSubSelector = function (val, prefix, suffix, constructSelector) { - var content = prefix; - val.forEach(function (subItem) { - content += constructSelector(subItem); - }); - - return content + suffix; -}; - -/** - * Constructs a syntax complete selector for our selector matching and warning output - * - * @param {object} val - The current node / part of our selector - * @returns {string} - Content: The current node with correct syntax e.g. class my-class = '.my-class' - */ -var constructSelector = function (val) { - var content = null; - - if (val.is('id')) { - content = addGrammar(val, '#', ''); - } - - else if (val.is('class')) { - content = addGrammar(val, '.', ''); - } - - else if (simpleIdents.indexOf(val.type) !== -1) { - content = val.content; - } - - else if (val.is('attributeSelector')) { - content = constructSubSelector(val, '[', ']', constructSelector); - } - - else if (val.is('atkeyword')) { - content = constructSubSelector(val, '@', '', constructSelector); - } - - else if (val.is('placeholder')) { - content = constructSubSelector(val, '%', '', constructSelector); - } - - else if (val.is('variable')) { - content = constructSubSelector(val, '$', '', constructSelector); - } - - else if (val.is('pseudoClass')) { - content = addGrammar(val, ':', ''); - } - - else if (val.is('pseudoElement')) { - content = addGrammar(val, '::', ''); - } - - else if (val.is('nth')) { - content = addGrammar(val, '(', ')'); - } - - else if (val.is('nthSelector')) { - content = constructSubSelector(val, ':', '', constructSelector); - } - - else if (val.is('parentheses')) { - content = constructSubSelector(val, '(', ')', constructSelector); - } - - else if (val.is('space')) { - content = ' '; - } - - else if (val.is('parentSelectorExtension') || val.is('attributeName') || val.is('attributeValue') || val.is('dimension')) { - content = constructSubSelector(val, '', '', constructSelector); - } - - else if (val.is('interpolation')) { - content = constructSubSelector(val, '#{', '}', constructSelector); - } - return content; -}; - /** * Traverses a block and calls our callback function for each block encountered * @@ -161,11 +58,11 @@ var checkRuleset = function (ruleNode) { if (!ruleNodeItem.is('block')) { if (ruleNodeItem.is('selector')) { ruleNodeItem.forEach(function (selectorContent) { - ruleSet += constructSelector(selectorContent); + ruleSet += selectorHelpers.constructSelector(selectorContent); }); } else if (ruleNodeItem.is('delimiter') || ruleNodeItem.is('space')) { - ruleSet += constructSelector(ruleNodeItem); + ruleSet += selectorHelpers.constructSelector(ruleNodeItem); } } }); @@ -184,7 +81,7 @@ var checkAtRule = function (atRule) { var test = ''; atRule.forEach(function (atRuleItem) { if (!atRuleItem.is('block')) { - test += constructSelector(atRuleItem); + test += selectorHelpers.constructSelector(atRuleItem); } }); updateList(test, true, atRule.start.line, atRule.start.column); diff --git a/lib/rules/no-url-domains.js b/lib/rules/no-url-domains.js new file mode 100644 index 00000000..73b491fe --- /dev/null +++ b/lib/rules/no-url-domains.js @@ -0,0 +1,33 @@ +'use strict'; + +var helpers = require('../helpers'), + url = require('url'); + +module.exports = { + 'name': 'no-url-domains', + 'defaults': {}, + 'detect': function (ast, parser) { + var result = []; + + ast.traverseByType('uri', function (uri) { + uri.traverse(function (item) { + if (item.is('string')) { + var stripped = helpers.stripQuotes(item.content), + parsedUrl = url.parse(stripped, false, true); + + if (parsedUrl.host && parsedUrl.protocol !== 'data:') { + result = helpers.addUnique(result, { + 'ruleId': parser.rule.name, + 'severity': parser.severity, + 'line': item.end.line, + 'column': item.end.column, + 'message': 'Domains in URLs are disallowed' + }); + } + } + }); + }); + + return result; + } +}; diff --git a/lib/rules/no-url-protocols.js b/lib/rules/no-url-protocols.js index 389b4e7a..90080479 100644 --- a/lib/rules/no-url-protocols.js +++ b/lib/rules/no-url-protocols.js @@ -2,30 +2,34 @@ var helpers = require('../helpers'); -var isUrlRegex = /^(https?:)?\/\//; - -var stripQuotes = function (str) { - return str.substring(1, str.length - 1); -}; +var isUrlRegex = /^(https?:)?\/\//, + protocolRelativeRegex = /^(https?:)\/\//; module.exports = { 'name': 'no-url-protocols', - 'defaults': {}, + 'defaults': { + 'allow-protocol-relative-urls': false + }, 'detect': function (ast, parser) { var result = []; ast.traverseByType('uri', function (uri) { uri.traverse(function (item) { if (item.is('string')) { - var stripped = stripQuotes(item.content); + var stripped = helpers.stripQuotes(item.content), + regexSelector = !parser.options['allow-protocol-relative-urls'] ? + isUrlRegex : protocolRelativeRegex, + message = !parser.options['allow-protocol-relative-urls'] ? + 'Protocols and domains in URLs are disallowed' : + 'Protocols in URLS are disallowed'; - if (stripped.match(isUrlRegex)) { + if (stripped.match(regexSelector)) { result = helpers.addUnique(result, { 'ruleId': parser.rule.name, 'severity': parser.severity, 'line': item.end.line, 'column': item.end.column, - 'message': 'Protocols and domains in URLs are disallowed' + 'message': message }); } } diff --git a/lib/rules/shorthand-values.js b/lib/rules/shorthand-values.js index 48e9ecd9..232ae45b 100644 --- a/lib/rules/shorthand-values.js +++ b/lib/rules/shorthand-values.js @@ -89,6 +89,10 @@ var scanValue = function (node) { fullVal += '#{' + scanValue(val.content) + '}'; } + else if (val.is('color')) { + fullVal += '#' + val.content + ''; + } + else if (val.is('operator') || val.is('ident') || val.is('number') || val.is('unaryOperator')) { fullVal += val.content; } diff --git a/lib/rules/single-line-per-selector.js b/lib/rules/single-line-per-selector.js index 9ba9c7ca..53cdf902 100644 --- a/lib/rules/single-line-per-selector.js +++ b/lib/rules/single-line-per-selector.js @@ -2,6 +2,31 @@ var helpers = require('../helpers'); +/** + * Checks a ruleset for selectors or EOL characters. If a selector is found before an EOL + * then it returns the selector node for reporting or returns false + * + * @param {Object} ruleset - The ruleset node + * @param {number} index - The current index of the delimiter + * @returns {Object|boolean} Either the selector node or false + */ +var checkLineForSelector = function (ruleset, index) { + var curIndex = index += 1; + if (ruleset.content[curIndex]) { + for (; curIndex < ruleset.content.length; curIndex++) { + var curType = ruleset.content[curIndex].type; + if (curType === 'space' && helpers.hasEOL(ruleset.content[curIndex])) { + return false; + } + if (curType === 'selector' || curType === 'typeSelector') { + return ruleset.content[curIndex]; + } + } + } + + return false; +}; + module.exports = { 'name': 'single-line-per-selector', 'defaults': {}, @@ -10,22 +35,16 @@ module.exports = { ast.traverseByType('ruleset', function (ruleset) { ruleset.forEach('delimiter', function (delimiter, j) { - var next = ruleset.content[j + 1] || false; + var next = checkLineForSelector(ruleset, j); if (next) { - if (next.is('selector')) { - next = next.content[0]; - } - - if (!(next.is('space') && helpers.hasEOL(next.content))) { - result = helpers.addUnique(result, { - 'ruleId': parser.rule.name, - 'line': next.start.line, - 'column': next.start.column, - 'message': 'Selectors must be placed on new lines', - 'severity': parser.severity - }); - } + result = helpers.addUnique(result, { + 'ruleId': parser.rule.name, + 'line': next.start.line, + 'column': next.start.column, + 'message': 'Selectors must be placed on new lines', + 'severity': parser.severity + }); } }); }); diff --git a/lib/selector-helpers.js b/lib/selector-helpers.js new file mode 100644 index 00000000..86b79b9b --- /dev/null +++ b/lib/selector-helpers.js @@ -0,0 +1,142 @@ +'use strict'; + +// ============================================================================== +// Helpers +// ============================================================================== + +var simpleIdents = [ + 'ident', + 'number', + 'operator', + 'combinator', + 'string', + 'parentSelector', + 'delimiter', + 'typeSelector', + 'attributeMatch' +]; + +var subSelectors = [ + 'parentSelectorExtension', + 'attributeName', + 'attributeValue', + 'dimension', + 'selector', + 'function' +]; + +/** + * Adds grammar around our content blocks to construct selectors with + * more readable formats. + * + * @param {object} val - The current value node + * @param {string} prefix - The grammar to prefix the value with + * @param {string} suffix - The grammar to add after the value + * @returns {string} The correct readable format + */ +var addGrammar = function (val, prefix, suffix) { + return prefix + val.content + suffix; +}; + +/** + * Adds grammar around our content blocks to construct selectors with + * more readable formats and loops the content as they're within sub blocks. + * + * @param {object} val - The current value node + * @param {string} prefix - The grammar to prefix the value with + * @param {string} suffix - The grammar to add after the value + * @param {function} constructSelector - The callback we wish to use which means constructSelector in this instance + * @returns {string} The correct readable format + */ +var constructSubSelector = function (val, prefix, suffix, constructSelector) { + var content = prefix; + val.forEach(function (subItem) { + content += constructSelector(subItem); + }); + + return content + suffix; +}; + +// ============================================================================== +// Public Methods +// ============================================================================== + +/** + * Constructs a syntax complete selector for our selector matching and warning output + * + * @param {object} val - The current node / part of our selector + * @returns {string} - Content: The current node with correct syntax e.g. class my-class = '.my-class' + */ +var constructSelector = function (val) { + var content = null; + + if (val.is('arguments')) { + content = constructSubSelector(val, '(', ')', constructSelector); + } + + else if (val.is('atkeyword')) { + content = constructSubSelector(val, '@', '', constructSelector); + } + + else if (val.is('attributeSelector')) { + content = constructSubSelector(val, '[', ']', constructSelector); + } + + else if (val.is('class')) { + content = addGrammar(val, '.', ''); + } + + else if (val.is('id')) { + content = addGrammar(val, '#', ''); + } + + else if (val.is('interpolation')) { + content = constructSubSelector(val, '#{', '}', constructSelector); + } + + else if (val.is('nth')) { + content = addGrammar(val, '(', ')'); + } + + else if (val.is('nthSelector')) { + content = constructSubSelector(val, ':', '', constructSelector); + } + + else if (val.is('parentheses')) { + content = constructSubSelector(val, '(', ')', constructSelector); + } + + else if (val.is('placeholder')) { + content = constructSubSelector(val, '%', '', constructSelector); + } + + else if (val.is('pseudoClass')) { + content = constructSubSelector(val, ':', '', constructSelector); + } + + else if (val.is('pseudoElement')) { + content = addGrammar(val, '::', ''); + } + + else if (val.is('space')) { + content = ' '; + } + + else if (val.is('variable')) { + content = constructSubSelector(val, '$', '', constructSelector); + } + + else if (simpleIdents.indexOf(val.type) !== -1) { + content = val.content; + } + + else if (subSelectors.indexOf(val.type) !== -1) { + content = constructSubSelector(val, '', '', constructSelector); + } + + return content; +}; + +module.exports = { + constructSelector: constructSelector +}; diff --git a/package.json b/package.json index 2b46c599..61f91546 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sass-lint", - "version": "1.8.2", + "version": "1.9.1", "description": "All Node Sass linter!", "main": "index.js", "scripts": { @@ -33,7 +33,7 @@ "fs-extra": "^0.30.0", "glob": "^7.0.0", "globule": "^1.0.0", - "gonzales-pe": "3.3.6", + "gonzales-pe": "3.4.4", "js-yaml": "^3.5.4", "lodash.capitalize": "^4.1.0", "lodash.kebabcase": "^4.0.0", @@ -45,7 +45,7 @@ "coveralls": "^2.11.4", "deep-equal": "^1.0.1", "istanbul": "^0.4.0", - "mocha": "^2.2.5", + "mocha": "^3.0.1", "sinon": "^1.17.4" } } diff --git a/tests/cli.js b/tests/cli.js index 4cc618c3..a3d6698d 100644 --- a/tests/cli.js +++ b/tests/cli.js @@ -383,27 +383,27 @@ describe('cli', function () { }); - it('should exit with exit code 1 when quiet', function (done) { + it('should exit with error when quiet', function (done) { var command = 'sass-lint -c tests/yml/.error-output.yml tests/cli/cli-error.scss --verbose --no-exit'; exec(command, function (err) { - if (err.code === 1) { + if (err) { return done(); } - return done(new Error('Error code not 1')); + return done(new Error('No error on exit')); }); }); - it('should exit with exit code 1 when more warnings than --max-warnings', function (done) { + it('should exit with error when more warnings than --max-warnings', function (done) { var command = 'sass-lint -c tests/yml/.color-keyword-errors.yml tests/cli/cli.scss --max-warnings 0'; exec(command, function (err) { - if (err && err.code === 1) { + if (err) { return done(); } - return done(new Error('Error code not 1')); + return done(new Error('No error on exit')); }); }); diff --git a/tests/failures.js b/tests/failures.js new file mode 100644 index 00000000..3127d451 --- /dev/null +++ b/tests/failures.js @@ -0,0 +1,90 @@ +'use strict'; + +var lint = require('../index'), + assert = require('assert'), + exceptions = require('../lib/exceptions'); + +describe('failures', function () { + it('should raise SassLintFailureError if indentation is set to error', function (done) { + assert.throws( + function () { + var results = lint.lintFiles('tests/sass/indentation/indentation-spaces.scss', {rules: {indentation: 2}}); // 14 errors + lint.failOnError(results); // Set indentation to error + }, + exceptions.SassLintFailureError + ); + assert.throws( + function () { + var results = lint.lintFiles('tests/sass/indentation/indentation-spaces.scss', {}, 'tests/yml/.indentation-error.yml'); // 14 errors + lint.failOnError(results); // Set indentation to error + }, + exceptions.SassLintFailureError + ); + + done(); + }); + + it('should not raise error if indentation is only set to warn', function (done) { + // These should produce 55 warnings and 0 errors + var directResults = lint.lintFiles('sass/indentation/indentation-spaces.scss', {rules: {indentation: 1}}); + var configResults = lint.lintFiles('sass/indentation/indentation-spaces.scss', {}, 'yml/.indentation-warn.yml'); + lint.failOnError(directResults); + lint.failOnError(configResults); + + done(); + }); + + it('should raise MaxWarningsExceededError if warnings exceed `max-warnings` setting', function (done) { + assert.throws( + function () { + var results = lint.lintFiles('tests/sass/indentation/indentation-spaces.scss', {}); // 55 warnings + lint.failOnError(results, {options: {'max-warnings': 10}}); + }, + exceptions.MaxWarningsExceededError + ); + assert.throws( + function () { + var results = lint.lintFiles('tests/sass/indentation/indentation-spaces.scss', {}); // 55 warnings + lint.failOnError(results, {}, 'tests/yml/.max-10-warnings.yml'); + }, + exceptions.MaxWarningsExceededError + ); + + done(); + }); + + it('should raise MaxWarningsExceededError if warnings exceed `max-warnings` of zero', function (done) { + assert.throws( + function () { + var results = lint.lintFiles('tests/sass/indentation/indentation-spaces.scss', {}); // 55 warnings + lint.failOnError(results, {options: {'max-warnings': 0}}); + }, + exceptions.MaxWarningsExceededError + ); + assert.throws( + function () { + var results = lint.lintFiles('tests/sass/indentation/indentation-spaces.scss', {}); // 55 warnings + lint.failOnError(results, {}, 'tests/yml/.max-0-warnings.yml'); + }, + exceptions.MaxWarningsExceededError + ); + + done(); + }); + + it('should not raise error if warnings do not exceed `max-warnings` setting', function (done) { + var results = lint.lintFiles('sass/indentation/indentation-spaces.scss', {}); // 55 warnings + lint.failOnError(results, {'max-warnings': 100}); // should succceed + lint.failOnError(results, {}, 'yml/.max-100-warnings.yml'); // should succeed + + done(); + }); + + it('should not raise error if no warnings even if `max-warnings` is zero', function (done) { + var results = lint.lintFiles('sass/success.scss', {}); // no warnings + lint.failOnError(results, {'max-warnings': 0}); // should still succceed + lint.failOnError(results, {}, 'yml/.max-0-warnings.yml'); // should still succeed + + done(); + }); +}); diff --git a/tests/rules/indentation.js b/tests/rules/indentation.js index 7ccf15f3..43968f51 100644 --- a/tests/rules/indentation.js +++ b/tests/rules/indentation.js @@ -13,7 +13,7 @@ describe('indentation - scss', function () { lint.test(spaceFile, { 'indentation': 1 }, function (data) { - lint.assert.equal(12, data.warningCount); + lint.assert.equal(14, data.warningCount); done(); }); }); @@ -27,7 +27,7 @@ describe('indentation - scss', function () { } ] }, function (data) { - lint.assert.equal(12, data.warningCount); + lint.assert.equal(14, data.warningCount); done(); }); }); diff --git a/tests/rules/no-color-hex.js b/tests/rules/no-color-hex.js new file mode 100644 index 00000000..2cfe53e5 --- /dev/null +++ b/tests/rules/no-color-hex.js @@ -0,0 +1,35 @@ +'use strict'; + +var lint = require('./_lint'); + +////////////////////////////// +// SCSS syntax tests +////////////////////////////// +describe('no color hex - scss', function () { + var file = lint.file('no-color-hex.scss'); + + it('enforce', function (done) { + lint.test(file, { + 'no-color-hex': 1 + }, function (data) { + lint.assert.equal(9, data.warningCount); + done(); + }); + }); +}); + +////////////////////////////// +// Sass syntax tests +////////////////////////////// +describe('no color hex - sass', function () { + var file = lint.file('no-color-hex.sass'); + + it('enforce', function (done) { + lint.test(file, { + 'no-color-hex': 1 + }, function (data) { + lint.assert.equal(9, data.warningCount); + done(); + }); + }); +}); diff --git a/tests/rules/no-mergeable-selectors.js b/tests/rules/no-mergeable-selectors.js index 449ad0e5..4b4be905 100644 --- a/tests/rules/no-mergeable-selectors.js +++ b/tests/rules/no-mergeable-selectors.js @@ -9,7 +9,7 @@ describe('no mergeable selectors - scss', function () { lint.test(file, { 'no-mergeable-selectors': 1 }, function (data) { - lint.assert.equal(22, data.warningCount); + lint.assert.equal(24, data.warningCount); done(); }); }); @@ -25,7 +25,7 @@ describe('no mergeable selectors - scss', function () { } ] }, function (data) { - lint.assert.equal(21, data.warningCount); + lint.assert.equal(23, data.warningCount); done(); }); }); @@ -40,7 +40,7 @@ describe('no mergeable selectors - sass', function () { lint.test(file, { 'no-mergeable-selectors': 1 }, function (data) { - lint.assert.equal(20, data.warningCount); + lint.assert.equal(21, data.warningCount); done(); }); }); @@ -57,7 +57,7 @@ describe('no mergeable selectors - sass', function () { } ] }, function (data) { - lint.assert.equal(19, data.warningCount); + lint.assert.equal(20, data.warningCount); done(); }); }); diff --git a/tests/rules/no-url-domains.js b/tests/rules/no-url-domains.js new file mode 100644 index 00000000..2caabdb1 --- /dev/null +++ b/tests/rules/no-url-domains.js @@ -0,0 +1,35 @@ +'use strict'; + +var lint = require('./_lint'); + +////////////////////////////// +// SCSS syntax tests +////////////////////////////// +describe('no url domains - scss', function () { + var file = lint.file('no-url-domains.scss'); + + it('enforce', function (done) { + lint.test(file, { + 'no-url-domains': 1 + }, function (data) { + lint.assert.equal(3, data.warningCount); + done(); + }); + }); +}); + +////////////////////////////// +// Sass syntax tests +////////////////////////////// +describe('no url domains - sass', function () { + var file = lint.file('no-url-domains.sass'); + + it('enforce', function (done) { + lint.test(file, { + 'no-url-domains': 1 + }, function (data) { + lint.assert.equal(3, data.warningCount); + done(); + }); + }); +}); diff --git a/tests/rules/no-url-protocols.js b/tests/rules/no-url-protocols.js index 87b5422a..65b5b5f1 100644 --- a/tests/rules/no-url-protocols.js +++ b/tests/rules/no-url-protocols.js @@ -16,8 +16,23 @@ describe('no url protocols - scss', function () { done(); }); }); + + it('[allow-protocol-relative-urls: true]', function (done) { + lint.test(file, { + 'no-url-protocols': [ + 1, + { + 'allow-protocol-relative-urls': true + } + ] + }, function (data) { + lint.assert.equal(2, data.warningCount); + done(); + }); + }); }); + ////////////////////////////// // Sass syntax tests ////////////////////////////// @@ -32,4 +47,18 @@ describe('no url protocols - sass', function () { done(); }); }); + + it('[allow-protocol-relative-urls: true]', function (done) { + lint.test(file, { + 'no-url-protocols': [ + 1, + { + 'allow-protocol-relative-urls': true + } + ] + }, function (data) { + lint.assert.equal(2, data.warningCount); + done(); + }); + }); }); diff --git a/tests/rules/shorthand-values.js b/tests/rules/shorthand-values.js index 7db7bafa..e80cc21c 100644 --- a/tests/rules/shorthand-values.js +++ b/tests/rules/shorthand-values.js @@ -12,7 +12,7 @@ describe('shorthand values - scss', function () { lint.test(file, { 'shorthand-values': 1 }, function (data) { - lint.assert.equal(76, data.warningCount); + lint.assert.equal(77, data.warningCount); done(); }); }); @@ -44,7 +44,7 @@ describe('shorthand values - scss', function () { } ] }, function (data) { - lint.assert.equal(38, data.warningCount); + lint.assert.equal(39, data.warningCount); done(); }); }); @@ -60,7 +60,7 @@ describe('shorthand values - scss', function () { } ] }, function (data) { - lint.assert.equal(45, data.warningCount); + lint.assert.equal(46, data.warningCount); done(); }); }); @@ -92,7 +92,7 @@ describe('shorthand values - scss', function () { } ] }, function (data) { - lint.assert.equal(57, data.warningCount); + lint.assert.equal(58, data.warningCount); done(); }); }); @@ -109,7 +109,7 @@ describe('shorthand values - scss', function () { } ] }, function (data) { - lint.assert.equal(64, data.warningCount); + lint.assert.equal(65, data.warningCount); done(); }); }); @@ -126,7 +126,7 @@ describe('shorthand values - scss', function () { } ] }, function (data) { - lint.assert.equal(57, data.warningCount); + lint.assert.equal(58, data.warningCount); done(); }); }); @@ -144,7 +144,7 @@ describe('shorthand values - scss', function () { } ] }, function (data) { - lint.assert.equal(76, data.warningCount); + lint.assert.equal(77, data.warningCount); done(); }); }); @@ -161,7 +161,7 @@ describe('shorthand values - sass', function () { lint.test(file, { 'shorthand-values': 1 }, function (data) { - lint.assert.equal(76, data.warningCount); + lint.assert.equal(77, data.warningCount); done(); }); }); @@ -193,7 +193,7 @@ describe('shorthand values - sass', function () { } ] }, function (data) { - lint.assert.equal(38, data.warningCount); + lint.assert.equal(39, data.warningCount); done(); }); }); @@ -209,7 +209,7 @@ describe('shorthand values - sass', function () { } ] }, function (data) { - lint.assert.equal(45, data.warningCount); + lint.assert.equal(46, data.warningCount); done(); }); }); @@ -241,7 +241,7 @@ describe('shorthand values - sass', function () { } ] }, function (data) { - lint.assert.equal(57, data.warningCount); + lint.assert.equal(58, data.warningCount); done(); }); }); @@ -258,7 +258,7 @@ describe('shorthand values - sass', function () { } ] }, function (data) { - lint.assert.equal(64, data.warningCount); + lint.assert.equal(65, data.warningCount); done(); }); }); @@ -275,7 +275,7 @@ describe('shorthand values - sass', function () { } ] }, function (data) { - lint.assert.equal(57, data.warningCount); + lint.assert.equal(58, data.warningCount); done(); }); }); @@ -293,7 +293,7 @@ describe('shorthand values - sass', function () { } ] }, function (data) { - lint.assert.equal(76, data.warningCount); + lint.assert.equal(77, data.warningCount); done(); }); }); diff --git a/tests/sass/indentation/indentation-spaces.scss b/tests/sass/indentation/indentation-spaces.scss index 52d83606..15d8ea6b 100644 --- a/tests/sass/indentation/indentation-spaces.scss +++ b/tests/sass/indentation/indentation-spaces.scss @@ -143,3 +143,33 @@ $colors: ( margin: 16px; } } + +// Issue #783 and issue #779 - at-rule throws off indentation of maps etc + +@import 'echo-base/defaults/breakpoints'; + +$textsizes: ( + xs: ( + s: 10px, + m: 10px + ), + s: ( + s: 12px, + m: 13px + ), +); + +@function em($pixels, $context: $font-size-base) { + @return #{($pixels / $context)}rem; +} + +$textsizes: ( + xs: ( + s: 10px, + m: 10px + ), + s: ( + s: 12px, + m: 13px + ), +); diff --git a/tests/sass/indentation/indentation-tabs.scss b/tests/sass/indentation/indentation-tabs.scss index d0bf98fa..c34f5d5a 100644 --- a/tests/sass/indentation/indentation-tabs.scss +++ b/tests/sass/indentation/indentation-tabs.scss @@ -143,3 +143,33 @@ $colors: ( margin: 16px; } } + +// Issue #783 and issue #779 - at-rule throws off indentation of maps etc + +@import 'echo-base/defaults/breakpoints'; + +$textsizes: ( + xs: ( + s: 10px, + m: 10px + ), + s: ( + s: 12px, + m: 13px + ), +); + +@function em($pixels, $context: $font-size-base) { + @return #{($pixels / $context)}rem; +} + +$textsizes: ( + xs: ( + s: 10px, + m: 10px + ), + s: ( + s: 12px, + m: 13px + ), +); diff --git a/tests/sass/no-color-hex.sass b/tests/sass/no-color-hex.sass new file mode 100644 index 00000000..69c26e65 --- /dev/null +++ b/tests/sass/no-color-hex.sass @@ -0,0 +1,25 @@ +$foo-color: #123 + +.foo + background: linear-gradient(top, #cc2, #44d) + color: #fff + + +$bar-color: #112233 + +.bar + background: linear-gradient(top, #cccc22, #4444dd) + color: #ffffff + +.baz + border-color: #123456 + + +// color literals, rgb and hsl values currently don't get returned +// by the AST's color type + +$qux-color: red +$rgb-color: rgb(255, 255, 255) +$rgba-color: rgba(0, 0, 0, .1) +$hsl-color: hsl(40, 50%, 50%) +$hsla-color: hsla(40, 50%, 50%, .3) diff --git a/tests/sass/no-color-hex.scss b/tests/sass/no-color-hex.scss new file mode 100644 index 00000000..58c383d4 --- /dev/null +++ b/tests/sass/no-color-hex.scss @@ -0,0 +1,26 @@ +$foo-color: #123; + +.foo { + background: linear-gradient(top, #cc2, #44d); + color: #fff; +} + +$bar-color: #112233; + +.bar { + background: linear-gradient(top, #cccc22, #4444dd); + color: #ffffff; +} + +.baz { + border-color: #123456; +} + +// color literals, rgb and hsl values currently don't get returned +// by the AST's color type + +$qux-color: red; +$rgb-color: rgb(255, 255, 255); +$rgba-color: rgba(0, 0, 0, .1); +$hsl-color: hsl(40, 50%, 50%); +$hsla-color: hsla(40, 50%, 50%, .3); diff --git a/tests/sass/no-mergeable-selectors.sass b/tests/sass/no-mergeable-selectors.sass index 1d7c6d36..04aad20d 100644 --- a/tests/sass/no-mergeable-selectors.sass +++ b/tests/sass/no-mergeable-selectors.sass @@ -179,6 +179,20 @@ ul ~ p .bar content: '' +// Issue #834 - selectors/typeselectors not properly recognised +.fake-field + tbody + tr:nth-child(even) + background: lighten($theme-color-primary, 50%) + tr:nth-child(odd) + background: #FFFFFF + +.not-test + &:not(:first-child) + border-left: none + + &:not(:first-child) + border-left: 2px .bar @media (max-width: 40em) and (min-width: 20em) and (orientation: landscape) @@ -237,15 +251,3 @@ ul ~ p // opacity: 1 // Issue #703 - Interpolation in selector - ignored in Sass syntax for now due to gonzales issue - -.navigation - @media #{$media-query-lg-up} - .nav-item - display: inline-block - @media #{$media-query-md-down} - // should not merge with the media query above - .nav-item - display: block - // should merge with the ruleset directly above - .nav-item - color: $blue diff --git a/tests/sass/no-mergeable-selectors.scss b/tests/sass/no-mergeable-selectors.scss index a16e7dbc..335c8889 100644 --- a/tests/sass/no-mergeable-selectors.scss +++ b/tests/sass/no-mergeable-selectors.scss @@ -320,3 +320,43 @@ ul ~ p { } } } + +// issue 826 - media queries with functions +@media(min-width: break('large')) { + .test { + float: left; + } +} + +@media(min-width: break('small')) { + .test { + float: left; + } +} + +@media(min-width: break('small')) { + .test { + float: none; + } +} + +// Issue #834 - selectors/typeselectors not properly recognised +.fake-field { + tbody { + tr:nth-child(even) { + background: lighten($theme-color-primary, 50%); + } + tr:nth-child(odd) { + background: #FFFFFF; + } + } +} + +.pseudo-not { + &:not(:first-child) { + border-left: none; + } + &:not(:first-child) { + border-left: 2px; + } +} diff --git a/tests/sass/no-url-domains.sass b/tests/sass/no-url-domains.sass new file mode 100644 index 00000000..21a17035 --- /dev/null +++ b/tests/sass/no-url-domains.sass @@ -0,0 +1,25 @@ +.foo + background-image: url('https://foo.com/img/bar.png') + + +.foo + background-image: url('http://foo.com/img/bar.png') + + +.foo + background-image: url('//foo.com/img/bar.png') + + +.foo + background-image: url('/img/bar.png') + + +.foo + background-image: url('img/bar.png') + + +.foo + background-image: url('bar.png') + +.foo + background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7') diff --git a/tests/sass/no-url-domains.scss b/tests/sass/no-url-domains.scss new file mode 100644 index 00000000..98ba24c0 --- /dev/null +++ b/tests/sass/no-url-domains.scss @@ -0,0 +1,27 @@ +.foo { + background-image: url('https://foo.com/img/bar.png'); +} + +.foo { + background-image: url('http://foo.com/img/bar.png'); +} + +.foo { + background-image: url('//foo.com/img/bar.png'); +} + +.foo { + background-image: url('/img/bar.png'); +} + +.foo { + background-image: url('img/bar.png'); +} + +.foo { + background-image: url('bar.png'); +} + +.foo { + background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); +} diff --git a/tests/sass/selector-helpers/selector-helpers.scss b/tests/sass/selector-helpers/selector-helpers.scss new file mode 100644 index 00000000..cd206336 --- /dev/null +++ b/tests/sass/selector-helpers/selector-helpers.scss @@ -0,0 +1,68 @@ +.test{ + color: red; +} + +#test{ + color: red; +} + +%test { + color: red; +} + +.#{test} { + color: red +} + +.test, #test { + color: red; +} + +input[type="text"] { + color: red; +} + +.test > li { + color: red; +} + +span[lang~=en-us] { + color: red; +} + +.block__element-one { + color: red; +} + +@media (max-width: 200px) { + color: red; +} + +##{$id} { + color: red; +} + +.right-element::-ms-backdrop { + content: "right-prefixed-element"; +} + +.wrong-element:selection { + content: "wrong-element"; +} + +p:nth-of-type(2) { + margin: 0; +} + +.test { + &__test { + color: red; + } +} + +tr:nth-child(even) { + background: lighten($theme-color-primary, 50%); +} +tr:nth-child(odd) { + background: #FFFFFF; +} diff --git a/tests/sass/shorthand-values.sass b/tests/sass/shorthand-values.sass index f21aa5b4..b766c6ce 100644 --- a/tests/sass/shorthand-values.sass +++ b/tests/sass/shorthand-values.sass @@ -297,3 +297,11 @@ .value-four-diff-four-interp-function-mixed margin: calc(#{$doc-header-height} + #{$global-whitespace--regular}) calc(#{$doc-header-height} + #{$global-whitespace--regular}) calc(#{$doc-header-height} - #{$global-whitespace--regular}) calc(#{$doc-header-height} - #{$global-whitespace--regular}) + +// issue #772 - Issue with colours not being correctly interpreted +// should be ignored; +.test + border-color: transparent transparent transparent #095b97 + +.test + border-color: transparent #095b97 transparent #095b97 diff --git a/tests/sass/shorthand-values.scss b/tests/sass/shorthand-values.scss index 925f5d3e..7ffbccfb 100644 --- a/tests/sass/shorthand-values.scss +++ b/tests/sass/shorthand-values.scss @@ -359,3 +359,13 @@ .value-four-diff-four-interp-function-mixed { margin: calc(#{$doc-header-height} + #{$global-whitespace--regular}) calc(#{$doc-header-height} + #{$global-whitespace--regular}) calc(#{$doc-header-height} - #{$global-whitespace--regular}) calc(#{$doc-header-height} - #{$global-whitespace--regular}); } + +// issue #772 - Issue with colours not being correctly interpreted +// should be ignored; +.test { + border-color: transparent transparent transparent #095b97; +} + +.test { + border-color: transparent #095b97 transparent #095b97; +} diff --git a/tests/sass/single-line-per-selector.sass b/tests/sass/single-line-per-selector.sass index 72210d79..f2b94b0f 100644 --- a/tests/sass/single-line-per-selector.sass +++ b/tests/sass/single-line-per-selector.sass @@ -48,3 +48,12 @@ .foo .bar &, .baz & content: 'foo' + +// Issue #789 - Issue with comments being warned as selector content on single lines +button, +html, +html input[type='button'], // 6 +input[type='reset'], +input[type='submit'] + -webkit-appearance: button; // 7 + cursor: pointer; // 8 diff --git a/tests/sass/single-line-per-selector.scss b/tests/sass/single-line-per-selector.scss index 65ed18ef..18fcb5df 100644 --- a/tests/sass/single-line-per-selector.scss +++ b/tests/sass/single-line-per-selector.scss @@ -54,3 +54,13 @@ content: 'foo'; } } + +// Issue #789 - Issue with comments being warned as selector content on single lines +button, +html, +html input[type='button'], // 6 +input[type='reset'], +input[type='submit'] { + -webkit-appearance: button; // 7 + cursor: pointer; // 8 +} diff --git a/tests/sass/success.scss b/tests/sass/success.scss new file mode 100644 index 00000000..ae771e48 --- /dev/null +++ b/tests/sass/success.scss @@ -0,0 +1,3 @@ +.one { + margin-top: 0; +} diff --git a/tests/selector-helpers/selectorHelpers.js b/tests/selector-helpers/selectorHelpers.js new file mode 100644 index 00000000..23ffe732 --- /dev/null +++ b/tests/selector-helpers/selectorHelpers.js @@ -0,0 +1,137 @@ +'use strict'; + +var assert = require('assert'), + selectorHelpers = require('../../lib/selector-helpers'), + groot = require('../../lib/groot'), + path = require('path'), + fs = require('fs'), + equal = require('deep-equal'); + +describe('selectorHelpers - constructSelector', function () { + + var expectedSelectors = [ + '.test', + '#test', + '%test', + '.#{test}', + '.test', + '#test', + 'input[type="text"]', + '.test > li', + 'span[lang~=en-us]', + '.block__element-one', + '##{$id}', + '.right-element::-ms-backdrop', + '.wrong-element:selection', + 'p:nth-of-type(2)', + '.test', + '&__test', + 'tr:nth-child(even)', + 'even', + 'tr:nth-child(odd)', + 'odd' + ], + selectorList = []; + + before(function () { + var file = '../sass/selector-helpers/selector-helpers.scss', + ast = groot(fs.readFileSync(path.join(__dirname, file)), path.extname(file).replace('.', ''), file); + + ast.traverseByType('selector', function (value) { + var ruleSet = ''; + value.forEach(function (selectorContent) { + ruleSet += selectorHelpers.constructSelector(selectorContent); + }); + selectorList.push(ruleSet); + }); + }); + + ////////////////////////////// + // contructSelector + ////////////////////////////// + + // .test + it('should return the correct class name', function (done) { + assert(equal(selectorList[0], expectedSelectors[0])); + done(); + }); + + // #test + it('should return the correct ID name', function (done) { + assert(equal(selectorList[1], expectedSelectors[1])); + done(); + }); + + // %test + it('should return the correct placeholder name', function (done) { + assert(equal(selectorList[2], expectedSelectors[2])); + done(); + }); + + // .#{test} + it('should return the correct interpolated selector name', function (done) { + assert(equal(selectorList[3], expectedSelectors[3])); + done(); + }); + + // input[type="text"] + it('should return the correct type selector name', function (done) { + assert(equal(selectorList[6], expectedSelectors[6])); + done(); + }); + + // .test > li + it('should return the correct combinator selector name', function (done) { + assert(equal(selectorList[7], expectedSelectors[7])); + done(); + }); + + // span[lang~=en-us] + it('should return the correct attribute selector name', function (done) { + assert(equal(selectorList[8], expectedSelectors[8])); + done(); + }); + + // .block__element-one + it('should return the correct BEM selector name', function (done) { + assert(equal(selectorList[9], expectedSelectors[9])); + done(); + }); + + // ##{$id} + it('should return the correct interpolated ID selector name', function (done) { + assert(equal(selectorList[10], expectedSelectors[10])); + done(); + }); + + // .right-element::-ms-backdrop + it('should return the correct pseudo element selector name', function (done) { + assert(equal(selectorList[11], expectedSelectors[11])); + done(); + }); + + // .wrong-element:selection + it('should return the correct pseudo selector name', function (done) { + assert(equal(selectorList[12], expectedSelectors[12])); + done(); + }); + + // p:nth-of-type(2) + it('should return the correct nth selector name', function (done) { + assert(equal(selectorList[13], expectedSelectors[13])); + done(); + }); + + // &__test + it('should return the correct parent selector name', function (done) { + assert(equal(selectorList[15], expectedSelectors[15])); + done(); + }); + + // tr:nth-child(even) + it('should return the correct nth selector and typeselector', function (done) { + assert(equal(selectorList[16], expectedSelectors[16])); + done(); + }); + +}); diff --git a/tests/yml/.indentation-error.yml b/tests/yml/.indentation-error.yml new file mode 100644 index 00000000..57dbbfe6 --- /dev/null +++ b/tests/yml/.indentation-error.yml @@ -0,0 +1,2 @@ +rules: + indentation: 2 diff --git a/tests/yml/.indentation-warn.yml b/tests/yml/.indentation-warn.yml new file mode 100644 index 00000000..71a1f08f --- /dev/null +++ b/tests/yml/.indentation-warn.yml @@ -0,0 +1,2 @@ +rules: + indentation: 1 diff --git a/tests/yml/.max-0-warnings.yml b/tests/yml/.max-0-warnings.yml new file mode 100644 index 00000000..0a3dc120 --- /dev/null +++ b/tests/yml/.max-0-warnings.yml @@ -0,0 +1,2 @@ +options: + max-warnings: 0 diff --git a/tests/yml/.max-10-warnings.yml b/tests/yml/.max-10-warnings.yml new file mode 100644 index 00000000..85b0f1c9 --- /dev/null +++ b/tests/yml/.max-10-warnings.yml @@ -0,0 +1,2 @@ +options: + max-warnings: 10 diff --git a/tests/yml/.max-100-warnings.yml b/tests/yml/.max-100-warnings.yml new file mode 100644 index 00000000..759639a6 --- /dev/null +++ b/tests/yml/.max-100-warnings.yml @@ -0,0 +1,2 @@ +options: + max-warnings: 100