diff --git a/src/rules/no-duplicate-dollar-variables/README.md b/src/rules/no-duplicate-dollar-variables/README.md index e8f0a2ca..4a038601 100644 --- a/src/rules/no-duplicate-dollar-variables/README.md +++ b/src/rules/no-duplicate-dollar-variables/README.md @@ -33,7 +33,23 @@ $a: 1; } ``` -The following patterns are *not* considered violations: +```scss +$a: 1; +.b { + .c { + $a: 1; + } +} +``` + +```scss +$a: 1; +@mixin b { + $a: 1; +} +``` + +The following patterns are _not_ considered violations: ```scss $a: 1; @@ -46,3 +62,82 @@ $a: 1; $b: 2; } ``` + +### `ignoreInside: ["at-rule", "nested-at-rule"]` + +#### `"at-rule"` + +Ignores dollar variables that are inside both nested and non-nested at-rules (`@media`, `@mixin`, etc.). + +Given: + +```json +{ "ignoreInside": ["at-rule"] } +``` + +The following patterns are _not_ considered warnings: + +```scss +$a: 1; +@mixin c { + $a: 1; +} +``` + +```scss +$a: 1; +.b { + @mixin c { + $a: 1; + } +} +``` + +#### `"nested-at-rule"` + +Ignores dollar variables that are inside nested at-rules (`@media`, `@mixin`, etc.). + +Given: + +```json +{ "ignoreInside": ["nested-at-rule"] } +``` + +The following patterns are _not_ considered warnings: + +```scss +$a: 1; +.b { + @mixin c { + $a: 1; + } +} +``` + +### `ignoreInsideAtRules: ["array", "of", "at-rules"]` + +Ignores all variables that are inside specified at-rules. + +Given: + +```json +{ "ignoreInsideAtRules": ["if", "mixin"] } +``` + +The following patterns are _not_ considered warnings: + +```scss +$a: 1; + +@mixin b { + $a: 2; +} +``` + +```scss +$a: 1; + +@if (true) { + $a: 2; +} +``` diff --git a/src/rules/no-duplicate-dollar-variables/__tests__/index.js b/src/rules/no-duplicate-dollar-variables/__tests__/index.js index 98d713d2..d4b76380 100644 --- a/src/rules/no-duplicate-dollar-variables/__tests__/index.js +++ b/src/rules/no-duplicate-dollar-variables/__tests__/index.js @@ -52,7 +52,8 @@ testRule(rule, { $b: 2; } `, - description: "Two dollar variables with different names and nesting." + description: + "Two dollar variables with different names and inside a selector." }, { code: ` @@ -138,7 +139,54 @@ testRule(rule, { line: 4, column: 9, message: messages.rejected("$ab"), - description: "Two dollar variables with the same name and nesting." + description: + "Two dollar variables with the same name and inside a selector." + }, + { + code: ` + $ab: 1; + .b { + @mixin c { + $ab: 2; + } + } + `, + line: 5, + column: 11, + message: messages.rejected("$ab"), + description: "Two dollar variables with the same name and nested @mixin." + }, + { + code: ` + $ab: 1; + .b { + & { + $ab: 2; + } + } + `, + line: 5, + column: 11, + message: messages.rejected("$ab"), + description: + "Two dollar variables with the same name and nesting selector." + }, + { + code: ` + $a: blue; + @mixin foo { + $a: red; + color: $a; + } + .b { + color: $a; + @include foo; + } + `, + line: 4, + column: 9, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name and @mixin." }, { code: ` @@ -177,3 +225,669 @@ testRule(rule, { } ] }); + +testRule(rule, { + ruleName, + config: [true, { ignoreInside: "at-rule" }], + syntax: "scss", + + accept: [ + { + code: ` + $a: 1; + + .a { + @mixin { + $a: 2; + } + } + `, + description: "Should ignore inside nested @mixin." + }, + { + code: ` + $ab: 1; + @mixin b { + $ab: 2; + } + `, + description: "Two dollar variables with the same name and inside @mixin." + }, + { + code: ` + $a: 1; + + .a { + .b .c { + @media (max-width: 500px) { + $a: 2; + } + } + } + `, + description: "Should ignore inside nested @media query." + }, + { + code: ` + $a: 1; + `, + description: "A single dollar variable." + }, + { + code: ` + $a: 1; + $b: 2; + `, + description: "Two dollar variables with different names." + }, + { + code: ` + $a: 1; $b: 2; + `, + description: "Two dollar variables with different names on the same line." + }, + { + code: "a { $a: 0; $b: $a + 1; }", + description: + "Two dollar variables with different names on the same line and variable assign." + }, + { + code: "a { @less: 0; @less: @less + 1; }", + description: "Less variables are ignored" + }, + { + code: "a { --custom-property: 0; --custom-property: 1; }", + description: "Custom properties are ignored" + }, + { + code: ` + $a: 1; + $ab: 2; + `, + description: "Two dollar variables with different names." + }, + { + code: ` + $a: 1; + .b { + $b: 2; + } + `, + description: "Two dollar variables with different names and nesting." + }, + { + code: ` + $a: 1; + .b { + color: black; + } + `, + description: "A single variable and a normal CSS property." + } + ], + + reject: [ + { + code: ` + $a: 1; + + .a { + .b { + $a: 2; + } + } + `, + line: 6, + column: 11, + message: messages.rejected("$a"), + description: "Should warn inside nested selector." + }, + { + code: ` + $a: 1; + + .a { + & { + $a: 2; + } + } + `, + line: 6, + column: 11, + message: messages.rejected("$a"), + description: "Should warn inside nesting selector." + }, + + { + code: ` + $a: 1; + $b: 2; + + .a { + $b: 3; + .b { + $a: 2; + } + } + `, + line: 6, + column: 9, + message: messages.rejected("$b"), + description: + "Should warn for a var inside the selector, but not for nested one." + }, + { + code: ` + $a: 1; + $a: 2; + `, + line: 3, + column: 7, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name." + }, + { + code: ` + $a: 1; $a: 2; + `, + line: 2, + column: 14, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name on the same line." + }, + { + code: ` + $a: 1; + $a: 2; + $a: 1; + `, + line: 3, + column: 7, + message: messages.rejected("$a"), + description: "Three dollar variables with the same name." + }, + { + code: "a { $scss: 0; $scss: $scss + 1; }", + line: 1, + column: 15, + message: messages.rejected("$scss"), + description: "Two dollar variables with the same name inside a selector." + }, + { + code: ` + $a: 1; + $b: 2; + $a: 3; + `, + line: 4, + column: 7, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name, variable between." + }, + { + code: ` + $a: 1; + + $b: 2; + + $a: 3; + `, + line: 6, + column: 7, + message: messages.rejected("$a"), + description: + "Two dollar variables with the same name, variable and newlines between." + }, + { + code: ` + $ab: 1; + .b { + $ab: 2; + } + `, + line: 4, + column: 9, + message: messages.rejected("$ab"), + description: + "Two dollar variables with the same name and inside a selector." + } + ] +}); + +testRule(rule, { + ruleName, + config: [true, { ignoreInside: "nested-at-rule" }], + syntax: "scss", + + accept: [ + { + code: ` + $a: 1; + + .a { + @mixin { + $a: 2; + } + } + `, + description: "Should ignore inside nested @mixin." + }, + { + code: ` + $a: 1; + + .a { + .b .c { + @media (max-width: 500px) { + $a: 2; + } + } + } + `, + description: "Should ignore inside nested @media query." + }, + { + code: ` + $a: 1; + `, + description: "A single dollar variable." + }, + { + code: ` + $a: 1; + $b: 2; + `, + description: "Two dollar variables with different names." + }, + { + code: ` + $a: 1; $b: 2; + `, + description: "Two dollar variables with different names on the same line." + }, + { + code: "a { $a: 0; $b: $a + 1; }", + description: + "Two dollar variables with different names on the same line and variable assign." + }, + { + code: "a { @less: 0; @less: @less + 1; }", + description: "Less variables are ignored" + }, + { + code: "a { --custom-property: 0; --custom-property: 1; }", + description: "Custom properties are ignored" + }, + { + code: ` + $a: 1; + $ab: 2; + `, + description: "Two dollar variables with different names." + }, + { + code: ` + $a: 1; + .b { + $b: 2; + } + `, + description: "Two dollar variables with different names and nesting." + }, + { + code: ` + $a: 1; + .b { + color: black; + } + `, + description: "A single variable and a normal CSS property." + } + ], + + reject: [ + { + code: ` + $a: 1; + + .a { + .b { + $a: 2; + } + } + `, + line: 6, + column: 11, + message: messages.rejected("$a"), + description: "Should warn inside nested selector." + }, + { + code: ` + $a: 1; + + .a { + & { + $a: 2; + } + } + `, + line: 6, + column: 11, + message: messages.rejected("$a"), + description: "Should warn inside nesting selector." + }, + + { + code: ` + $a: 1; + $b: 2; + + .a { + $b: 3; + .b { + $a: 2; + } + } + `, + line: 6, + column: 9, + message: messages.rejected("$b"), + description: + "Should warn for a var inside the selector, but not for nested one." + }, + { + code: ` + $a: 1; + $a: 2; + `, + line: 3, + column: 7, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name." + }, + { + code: ` + $a: 1; $a: 2; + `, + line: 2, + column: 14, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name on the same line." + }, + { + code: ` + $a: 1; + $a: 2; + $a: 1; + `, + line: 3, + column: 7, + message: messages.rejected("$a"), + description: "Three dollar variables with the same name." + }, + { + code: "a { $scss: 0; $scss: $scss + 1; }", + line: 1, + column: 15, + message: messages.rejected("$scss"), + description: "Two dollar variables with the same name inside a selector." + }, + { + code: ` + $a: 1; + $b: 2; + $a: 3; + `, + line: 4, + column: 7, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name, variable between." + }, + { + code: ` + $a: 1; + + $b: 2; + + $a: 3; + `, + line: 6, + column: 7, + message: messages.rejected("$a"), + description: + "Two dollar variables with the same name, variable and newlines between." + }, + { + code: ` + $ab: 1; + .b { + $ab: 2; + } + `, + line: 4, + column: 9, + message: messages.rejected("$ab"), + description: + "Two dollar variables with the same name and inside a selector." + }, + { + code: ` + $ab: 1; + @mixin b { + $ab: 2; + } + `, + line: 4, + column: 9, + message: messages.rejected("$ab"), + description: "Two dollar variables with the same name and inside @mixin." + } + ] +}); + +testRule(rule, { + ruleName, + config: [true, { ignoreInsideAtRules: ["if", "mixin"] }], + syntax: "scss", + + accept: [ + { + code: ` + $a: 1; + + @mixin b { + $a: 2; + } + `, + description: "Should ignore inside @mixin." + }, + { + code: ` + $a: 1; + + .a { + .b { + @mixin c { + $a: 2; + } + } + } + + `, + description: "Should ignore inside nested @mixin." + }, + { + code: ` + $a: 1; + + @if (true) { + $a: 2; + } + `, + description: "Should ignore inside @if." + }, + { + code: ` + $a: 1; + `, + description: "A single dollar variable." + }, + { + code: ` + $a: 1; + $b: 2; + `, + description: "Two dollar variables with different names." + }, + { + code: ` + $a: 1; $b: 2; + `, + description: "Two dollar variables with different names on the same line." + }, + { + code: "a { $a: 0; $b: $a + 1; }", + description: + "Two dollar variables with different names on the same line and variable assign." + }, + { + code: "a { @less: 0; @less: @less + 1; }", + description: "Less variables are ignored" + }, + { + code: "a { --custom-property: 0; --custom-property: 1; }", + description: "Custom properties are ignored" + }, + { + code: ` + $a: 1; + $ab: 2; + `, + description: "Two dollar variables with different names." + }, + { + code: ` + $a: 1; + .b { + $b: 2; + } + `, + description: + "Two dollar variables with different names and inside a selector." + }, + { + code: ` + $a: 1; + .b { + color: black; + } + `, + description: "A single variable and a normal CSS property." + } + ], + reject: [ + { + code: ` + $c: 1; + + @function b() { + $c: 2; + } + `, + line: 5, + column: 9, + message: messages.rejected("$c"), + description: "Should warn inside @function as it is not ignored." + }, + { + code: ` + $a: 1; + $a: 2; + `, + line: 3, + column: 7, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name." + }, + { + code: ` + $a: 1; $a: 2; + `, + line: 2, + column: 14, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name on the same line." + }, + { + code: ` + $a: 1; + $a: 2; + $a: 1; + `, + line: 3, + column: 7, + message: messages.rejected("$a"), + description: "Three dollar variables with the same name." + }, + { + code: "a { $scss: 0; $scss: $scss + 1; }", + line: 1, + column: 15, + message: messages.rejected("$scss"), + description: "Two dollar variables with the same name inside a selector." + }, + { + code: ` + $a: 1; + $b: 2; + $a: 3; + `, + line: 4, + column: 7, + message: messages.rejected("$a"), + description: "Two dollar variables with the same name, variable between." + }, + { + code: ` + $a: 1; + + $b: 2; + + $a: 3; + `, + line: 6, + column: 7, + message: messages.rejected("$a"), + description: + "Two dollar variables with the same name, variable and newlines between." + }, + { + code: ` + $ab: 1; + .b { + $ab: 2; + } + `, + line: 4, + column: 9, + message: messages.rejected("$ab"), + description: + "Two dollar variables with the same name and inside a selector." + }, + { + code: ` + $ab: 1; + .b { + & { + $ab: 2; + } + } + `, + line: 5, + column: 11, + message: messages.rejected("$ab"), + description: + "Two dollar variables with the same name and nesting selector." + } + ] +}); diff --git a/src/rules/no-duplicate-dollar-variables/index.js b/src/rules/no-duplicate-dollar-variables/index.js index 72fb85da..ce8698f8 100644 --- a/src/rules/no-duplicate-dollar-variables/index.js +++ b/src/rules/no-duplicate-dollar-variables/index.js @@ -1,4 +1,5 @@ import { utils } from "stylelint"; +import { isString } from "lodash"; import { namespace } from "../../utils"; export const ruleName = namespace("no-duplicate-dollar-variables"); @@ -7,11 +8,23 @@ export const messages = utils.ruleMessages(ruleName, { rejected: variable => `Unexpected duplicate dollar variable ${variable}` }); -export default function(value) { +export default function(value, secondaryOptions) { return (root, result) => { - const validOptions = utils.validateOptions(result, ruleName, { - actual: value - }); + const validOptions = utils.validateOptions( + result, + ruleName, + { + actual: value + }, + { + actual: secondaryOptions, + possible: { + ignoreInside: ["at-rule", "nested-at-rule"], + ignoreInsideAtRules: [isString] + }, + optional: true + } + ); if (!validOptions) { return; @@ -20,7 +33,30 @@ export default function(value) { const vars = {}; root.walkDecls(decl => { - if (decl.prop[0] !== "$") { + const isVar = decl.prop[0] === "$"; + const isInsideIgnoredAtRule = + decl.parent.type === "atrule" && + secondaryOptions && + secondaryOptions.ignoreInside && + secondaryOptions.ignoreInside === "at-rule"; + const isInsideIgnoredNestedAtRule = + decl.parent.type === "atrule" && + decl.parent.parent.type !== "root" && + secondaryOptions && + secondaryOptions.ignoreInside && + secondaryOptions.ignoreInside === "nested-at-rule"; + const isInsideIgnoredSpecifiedAtRule = + decl.parent.type === "atrule" && + secondaryOptions && + secondaryOptions.ignoreInsideAtRules && + secondaryOptions.ignoreInsideAtRules.indexOf(decl.parent.name) > -1; + + if ( + !isVar || + isInsideIgnoredAtRule || + isInsideIgnoredNestedAtRule || + isInsideIgnoredSpecifiedAtRule + ) { return; }