Repository: stylelint-scss/stylelint-scss
Pull request #923 (2023-12-12 18:12:00)

Fix `selector-no-union-class-name` end positions
Part of #652 


Patch:
diff --git a/src/rules/at-import-partial-extension/index.js b/src/rules/at-import-partial-extension/index.js
index 9ca1aab..d4feed7 100644
--- a/src/rules/at-import-partial-extension/index.js
+++ b/src/rules/at-import-partial-extension/index.js
@@ -2,6 +2,7 @@
 
 const nodeJsPath = require("path");
 const { utils } = require("stylelint");
+const atRuleParamIndex = require("../../utils/atRuleParamIndex");
 const namespace = require("../../utils/namespace");
 const ruleUrl = require("../../utils/ruleUrl");
 
@@ -46,8 +47,8 @@ function rule(expectation, _, context) {
       return;
     }
 
-    root.walkAtRules("import", decl => {
-      const paths = decl.params
+    root.walkAtRules("import", atRule => {
+      const paths = atRule.params
         .split(/["']\s*,/)
         .filter(path => !mediaQueryTypesRE.test(path.trim()));
 
@@ -70,9 +71,10 @@ function rule(expectation, _, context) {
         if (!extension && expectation === "always") {
           utils.report({
             message: messages.expected,
-            node: decl,
+            node: atRule,
             result,
-            ruleName
+            ruleName,
+            word: pathStripped
           });
 
           return;
@@ -82,15 +84,20 @@ function rule(expectation, _, context) {
         if (extension && isScssPartial && expectation === "never") {
           if (context.fix) {
             const extPattern = new RegExp(`\\.${extension}(['" ]*)$`, "g");
-            decl.params = decl.params.replace(extPattern, "$1");
+            atRule.params = atRule.params.replace(extPattern, "$1");
 
             return;
           }
 
+          const dotExt = `.${extension}`;
+          const index =
+            atRuleParamIndex(atRule) + atRule.params.lastIndexOf(dotExt);
+
           utils.report({
             message: messages.rejected(extension),
-            node: decl,
-            word: extension,
+            node: atRule,
+            index,
+            endIndex: index + dotExt.length,
             result,
             ruleName
           });
diff --git a/src/rules/at-mixin-pattern/index.js b/src/rules/at-mixin-pattern/index.js
index 9a6634a..b5b7e8d 100644
--- a/src/rules/at-mixin-pattern/index.js
+++ b/src/rules/at-mixin-pattern/index.js
@@ -28,28 +28,20 @@ function rule(pattern) {
 
     const regexpPattern = isString(pattern) ? new RegExp(pattern) : pattern;
 
-    root.walkAtRules(decl => {
-      if (decl.name !== "mixin") {
-        return;
-      }
-
+    root.walkAtRules("mixin", atRule => {
       // Stripping the mixin of its arguments
-      const mixinName = decl.params.replace(/(\s*)\([\s\S]*\)/g, "");
+      const mixinName = atRule.params.replace(/(\s*)\([\s\S]*\)/g, "");
 
       if (regexpPattern.test(mixinName)) {
         return;
       }
 
-      const mixinTopLine = Object.assign({}, decl.source.start);
-      mixinTopLine.line += 1;
-      mixinTopLine.column = 0;
-
       utils.report({
         message: messages.expected,
-        node: decl,
+        node: atRule,
         result,
         ruleName,
-        end: mixinTopLine
+        word: mixinName
       });
     });
   };
diff --git a/src/rules/at-root-no-redundant/index.js b/src/rules/at-root-no-redundant/index.js
index 04fba04..1a7ded8 100644
--- a/src/rules/at-root-no-redundant/index.js
+++ b/src/rules/at-root-no-redundant/index.js
@@ -53,7 +53,8 @@ function rule(actual, _, context) {
           message: messages.rejected,
           node,
           result,
-          ruleName
+          ruleName,
+          word: `@${node.name}`
         });
       }
     });
diff --git a/src/rules/at-use-no-redundant-alias/index.js b/src/rules/at-use-no-redundant-alias/index.js
index 2ee2840..3da1f16 100644
--- a/src/rules/at-use-no-redundant-alias/index.js
+++ b/src/rules/at-use-no-redundant-alias/index.js
@@ -1,6 +1,7 @@
 "use strict";
 
 const { utils } = require("stylelint");
+const atRuleParamIndex = require("../../utils/atRuleParamIndex");
 const namespace = require("../../utils/namespace");
 const ruleUrl = require("../../utils/ruleUrl");
 
@@ -32,20 +33,27 @@ function rule(actual, _, context) {
       return;
     }
 
-    root.walkAtRules("use", decl => {
-      const parts = separateEachParams(decl.params);
+    root.walkAtRules("use", atRule => {
+      const parts = separateEachParams(atRule.params);
       if (parts && getDefaultNamespace(parts[0]) === parts[1]) {
         if (context.fix) {
-          decl.after(decl.toString().replace(/\s*as\s* [^\s*]+\s*/, " "));
-          decl.next().raws = decl.raws;
-          decl.remove();
+          atRule.after(atRule.toString().replace(/\s*as\s* [^\s*]+\s*/, " "));
+          atRule.next().raws = atRule.raws;
+          atRule.remove();
           return;
         }
+
+        const matchedAlias = atRule.params.match(/as\s+\S+/);
+        if (!matchedAlias) return;
+
+        const index = atRuleParamIndex(atRule) + matchedAlias.index;
         utils.report({
           message: messages.rejected,
-          node: decl,
+          node: atRule,
           result,
-          ruleName
+          ruleName,
+          index,
+          endIndex: index + matchedAlias[0].length
         });
       }
     });
diff --git a/src/rules/at-use-no-unnamespaced/index.js b/src/rules/at-use-no-unnamespaced/index.js
index 2c3503e..a943ff6 100644
--- a/src/rules/at-use-no-unnamespaced/index.js
+++ b/src/rules/at-use-no-unnamespaced/index.js
@@ -22,15 +22,17 @@ function rule(actual) {
       return;
     }
 
-    root.walkAtRules("use", decl => {
-      if (/as\s*\*\s*(?:$|with\s*\()/.test(decl.params)) {
-        utils.report({
-          message: messages.rejected,
-          node: decl,
-          result,
-          ruleName
-        });
-      }
+    root.walkAtRules("use", atRule => {
+      const matched = atRule.params.match(/(as\s*\*)\s*(?:$|with\s*\()/);
+      if (!matched) return;
+
+      utils.report({
+        message: messages.rejected,
+        node: atRule,
+        result,
+        ruleName,
+        word: matched[1]
+      });
     });
   };
 }
diff --git a/src/rules/dimension-no-non-numeric-values/index.js b/src/rules/dimension-no-non-numeric-values/index.js
index 0d21e74..1526431 100644
--- a/src/rules/dimension-no-non-numeric-values/index.js
+++ b/src/rules/dimension-no-non-numeric-values/index.js
@@ -2,7 +2,6 @@
 
 const valueParser = require("postcss-value-parser");
 const { utils } = require("stylelint");
-const declarationValueIndex = require("../../utils/declarationValueIndex");
 const namespace = require("../../utils/namespace");
 const ruleUrl = require("../../utils/ruleUrl");
 
@@ -107,13 +106,11 @@ function rule(primary) {
         }
 
         const unit = matchUnit[1];
-        const offset = decl.value.indexOf(unit);
-
         utils.report({
           ruleName,
           result,
           message: messages.rejected(unit),
-          index: declarationValueIndex(decl) + offset,
+          word: matchUnit[0],
           node: decl
         });
       });
diff --git a/src/rules/dollar-variable-no-missing-interpolation/index.js b/src/rules/dollar-variable-no-missing-interpolation/index.js
index d64bc8f..70e53da 100644
--- a/src/rules/dollar-variable-no-missing-interpolation/index.js
+++ b/src/rules/dollar-variable-no-missing-interpolation/index.js
@@ -111,7 +111,8 @@ function rule(actual) {
         ruleName,
         result,
         node,
-        message: messages.rejected(nodeName, value)
+        message: messages.rejected(nodeName, value),
+        word: value
       });
     }
 
diff --git a/src/rules/dollar-variable-pattern/index.js b/src/rules/dollar-variable-pattern/index.js
index 81b4448..f4ecdea 100644
--- a/src/rules/dollar-variable-pattern/index.js
+++ b/src/rules/dollar-variable-pattern/index.js
@@ -64,7 +64,8 @@ function rule(pattern, options) {
         message: messages.expected,
         node: decl,
         result,
-        ruleName
+        ruleName,
+        word: prop
       });
     });
   };
diff --git a/src/rules/function-calculation-no-interpolation/index.js b/src/rules/function-calculation-no-interpolation/index.js
index 0d3902e..a438256 100644
--- a/src/rules/function-calculation-no-interpolation/index.js
+++ b/src/rules/function-calculation-no-interpolation/index.js
@@ -27,23 +27,22 @@ function rule(actual) {
 
     root.walkDecls(decl => {
       valueParser(decl.value).walk(node => {
-        if (node.type !== "function" || node.value === "") {
-          return;
-        }
-        if (
-          calculationFunctions.includes(node.value) &&
-          node.nodes.some(
-            args => args.type === "word" && /^#{.*|\s*}/.test(args.value)
-          )
-        ) {
-          utils.report({
-            message: messages.rejected(node.value),
-            node: decl,
-            word: decl.name,
-            result,
-            ruleName
-          });
-        }
+        if (node.type !== "function" || node.value === "") return;
+
+        if (!calculationFunctions.includes(node.value)) return;
+
+        const interpolation = node.nodes.find(
+          ({ type, value }) => type === "word" && /^#{.*|\s*}/.test(value)
+        );
+        if (!interpolation) return;
+
+        utils.report({
+          message: messages.rejected(node.value),
+          node: decl,
+          word: interpolation.value,
+          result,
+          ruleName
+        });
       });
     });
   };
diff --git a/src/rules/function-color-relative/index.js b/src/rules/function-color-relative/index.js
index 86c4718..884cc40 100644
--- a/src/rules/function-color-relative/index.js
+++ b/src/rules/function-color-relative/index.js
@@ -42,6 +42,8 @@ function rule(primary) {
     }
 
     root.walkDecls(decl => {
+      const declValueIndex = declarationValueIndex(decl);
+
       valueParser(decl.value).walk(node => {
         // Verify that we're only looking at functions.
         if (node.type !== "function" || node.value === "") {
@@ -56,15 +58,17 @@ function rule(primary) {
           node.nodes.some(isColorFunction);
 
         if (isSassColorFunction || isDSFilterColorFunction) {
-          const nodes = isDSFilterColorFunction
+          const funcNodes = isDSFilterColorFunction
             ? node.nodes.filter(isColorFunction)
             : [node];
 
-          nodes.forEach(node => {
+          funcNodes.forEach(funcNode => {
+            const index = declValueIndex + funcNode.sourceIndex;
             utils.report({
               message: messages.rejected,
               node: decl,
-              index: declarationValueIndex(decl) + node.sourceIndex,
+              index,
+              endIndex: index + funcNode.value.length,
               result,
               ruleName
             });
diff --git a/src/rules/function-disallowed-list/index.js b/src/rules/function-disallowed-list/index.js
index fd9cb9c..dfc331b 100644
--- a/src/rules/function-disallowed-list/index.js
+++ b/src/rules/function-disallowed-list/index.js
@@ -51,7 +51,7 @@ function rule(disallowedOption) {
             utils.report({
               message: messages.rejected(nameWithoutNamespace),
               node: decl,
-              word: decl.name,
+              word: nameWithoutNamespace,
               result,
               ruleName
             });
diff --git a/src/rules/load-no-partial-leading-underscore/index.js b/src/rules/load-no-partial-leading-underscore/index.js
index 4272982..2fa5ad1 100644
--- a/src/rules/load-no-partial-leading-underscore/index.js
+++ b/src/rules/load-no-partial-leading-underscore/index.js
@@ -17,13 +17,14 @@ const meta = {
 function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
-    const hasArgumentsRegExp = /\(\s*([^)]+?)\s*\)/;
 
     if (!validOptions) {
       return;
     }
 
-    function checkPathForUnderscore(path, decl) {
+    const hasArgumentsRegExp = /\(\s*([^)]+?)\s*\)/;
+
+    function checkPathForUnderscore(path, atrule) {
       // Stripping trailing quotes and whitespaces, if any
       const pathStripped = path
         .replace(/^\s*(["'])\s*/, "")
@@ -46,9 +47,10 @@ function rule(actual) {
 
       utils.report({
         message: messages.expected,
-        node: decl,
+        node: atrule,
         result,
-        ruleName
+        ruleName,
+        word: pathStripped
       });
     }
 
@@ -69,7 +71,7 @@ function rule(actual) {
           const args = hasArgumentsRegExp.exec(atrule.params);
           if (args) {
             const arg = args[0].split(",");
-            checkPathForUnderscore(arg[0].replace(/\(|\)/, ""), atrule);
+            checkPathForUnderscore(arg[0].replace(/[()]/g, ""), atrule);
           }
         }
       }
diff --git a/src/rules/map-keys-quotes/index.js b/src/rules/map-keys-quotes/index.js
index b5952c7..e273d31 100644
--- a/src/rules/map-keys-quotes/index.js
+++ b/src/rules/map-keys-quotes/index.js
@@ -42,17 +42,18 @@ function rule(primary) {
           // Identify all of the map-keys and see if they're strings (not words).
           const mapKeys = returnMapKeys(node.nodes);
 
-          mapKeys.forEach(map_key => {
-            if (mathOperators.includes(map_key.value)) {
+          mapKeys.forEach(mapKey => {
+            if (mathOperators.includes(mapKey.value)) {
               return;
             }
 
-            if (map_key.type === "word" && isNaN(map_key.value)) {
+            if (mapKey.type === "word" && isNaN(mapKey.value)) {
               utils.report({
                 message: messages.expected,
                 node: decl,
                 result,
-                ruleName
+                ruleName,
+                word: mapKey.value
               });
             }
           });
diff --git a/src/rules/no-dollar-variables/index.js b/src/rules/no-dollar-variables/index.js
index a07724f..d4190ed 100644
--- a/src/rules/no-dollar-variables/index.js
+++ b/src/rules/no-dollar-variables/index.js
@@ -33,7 +33,8 @@ function rule(value) {
         message: messages.rejected(decl.prop),
         node: decl,
         result,
-        ruleName
+        ruleName,
+        word: decl.prop
       });
     });
   };
diff --git a/src/rules/no-duplicate-dollar-variables/index.js b/src/rules/no-duplicate-dollar-variables/index.js
index e38ba55..ce7109c 100644
--- a/src/rules/no-duplicate-dollar-variables/index.js
+++ b/src/rules/no-duplicate-dollar-variables/index.js
@@ -174,7 +174,8 @@ function rule(value, secondaryOptions) {
           message: messages.rejected(decl.prop),
           node: decl,
           result,
-          ruleName
+          ruleName,
+          word: decl.prop
         });
       }
 
diff --git a/src/rules/no-duplicate-mixins/index.js b/src/rules/no-duplicate-mixins/index.js
index a1f8413..905d94f 100644
--- a/src/rules/no-duplicate-mixins/index.js
+++ b/src/rules/no-duplicate-mixins/index.js
@@ -27,21 +27,16 @@ function rule(value) {
 
     const mixins = {};
 
-    root.walkAtRules(decl => {
-      const isMixin = decl.name === "mixin";
-
-      if (!isMixin) {
-        return;
-      }
-
-      const mixinName = atRuleBaseName(decl);
+    root.walkAtRules("mixin", atRule => {
+      const mixinName = atRuleBaseName(atRule);
 
       if (mixins[mixinName]) {
         utils.report({
           message: messages.rejected(mixinName),
-          node: decl,
+          node: atRule,
           result,
-          ruleName
+          ruleName,
+          word: mixinName
         });
       }
 
diff --git a/src/rules/no-global-function-names/index.js b/src/rules/no-global-function-names/index.js
index 9d750d7..0fa707e 100644
--- a/src/rules/no-global-function-names/index.js
+++ b/src/rules/no-global-function-names/index.js
@@ -2,7 +2,6 @@
 
 const valueParser = require("postcss-value-parser");
 const { utils } = require("stylelint");
-const declarationValueIndex = require("../../utils/declarationValueIndex");
 const namespace = require("../../utils/namespace");
 const ruleUrl = require("../../utils/ruleUrl");
 
@@ -182,11 +181,11 @@ function rule(value) {
           return;
         }
 
-        if (Object.keys(rules).includes(cleanValue)) {
+        if (rules[cleanValue]) {
           utils.report({
             message: messages.rejected(cleanValue),
             node: decl,
-            index: declarationValueIndex(decl) + node.sourceIndex,
+            word: cleanValue,
             result,
             ruleName
           });
diff --git a/src/rules/partial-no-import/index.js b/src/rules/partial-no-import/index.js
index 851181c..cf9aa23 100644
--- a/src/rules/partial-no-import/index.js
+++ b/src/rules/partial-no-import/index.js
@@ -36,7 +36,7 @@ function rule(on) {
     const fileName = nodeJsPath.basename(root.source.input.file);
     const extName = nodeJsPath.extname(root.source.input.file);
 
-    function checkImportForCSS(path, decl) {
+    function checkImportForCSS(path, atRule) {
       // Stripping trailing quotes and whitespaces, if any
       const pathStripped = path
         .replace(/^\s*(["'])\s*/, "")
@@ -55,8 +55,8 @@ function rule(on) {
 
       utils.report({
         message: messages.expected,
-        node: decl,
-        index: decl.params.indexOf(path),
+        node: atRule,
+        word: pathStripped,
         result,
         ruleName
       });
diff --git a/src/rules/percent-placeholder-pattern/index.js b/src/rules/percent-placeholder-pattern/index.js
index 767e2e7..143f0e4 100644
--- a/src/rules/percent-placeholder-pattern/index.js
+++ b/src/rules/percent-placeholder-pattern/index.js
@@ -70,7 +70,7 @@ function rule(pattern) {
     function checkSelector(fullSelector, rule) {
       // postcss-selector-parser gives %placeholders' nodes a "tag" type
       fullSelector.walkTags(compoundSelector => {
-        const { value, sourceIndex } = compoundSelector;
+        const { value } = compoundSelector;
 
         if (value[0] !== "%") {
           return;
@@ -87,7 +87,7 @@ function rule(pattern) {
           ruleName,
           message: messages.expected(placeholder),
           node: rule,
-          index: sourceIndex
+          word: value
         });
       });
     }
diff --git a/src/rules/selector-nest-combinators/index.js b/src/rules/selector-nest-combinators/index.js
index 05a5468..7c2ad85 100644
--- a/src/rules/selector-nest-combinators/index.js
+++ b/src/rules/selector-nest-combinators/index.js
@@ -71,8 +71,6 @@ function rule(expectation) {
       }
 
       parseSelector(rule.selector, result, rule, fullSelector => {
-        let message;
-
         fullSelector.walk(node => {
           if (node.value === "}") {
             return;
@@ -130,11 +128,21 @@ function rule(expectation) {
               return;
             }
 
+            let message;
             if (hasInterpolation) {
               message = messages.expectedInterpolation;
             } else {
               message = messages.expected(node.value, node.type);
             }
+
+            utils.report({
+              ruleName,
+              result,
+              node: rule,
+              message,
+              word: node.toString()
+            });
+            return;
           }
 
           if (expectation === "never") {
@@ -142,16 +150,17 @@ function rule(expectation) {
               return;
             }
 
-            message = messages.rejected;
-          }
+            if (node.type !== "selector") return;
 
-          utils.report({
-            ruleName,
-            result,
-            node: rule,
-            message,
-            index: node.sourceIndex
-          });
+            utils.report({
+              ruleName,
+              result,
+              node: rule,
+              message: messages.rejected,
+              word: node.toString()
+            });
+            return;
+          }
         });
       });
     });
diff --git a/src/rules/selector-no-redundant-nesting-selector/index.js b/src/rules/selector-no-redundant-nesting-selector/index.js
index 02c76e8..4236fef 100644
--- a/src/rules/selector-no-redundant-nesting-selector/index.js
+++ b/src/rules/selector-no-redundant-nesting-selector/index.js
@@ -77,7 +77,8 @@ function rule(actual, options) {
             result,
             node: rule,
             message: messages.rejected,
-            index: node.sourceIndex
+            index: node.sourceIndex,
+            endIndex: node.sourceIndex + node.value.length
           });
         });
       });
diff --git a/src/rules/selector-no-union-class-name/index.js b/src/rules/selector-no-union-class-name/index.js
index d510f9a..97eab54 100644
--- a/src/rules/selector-no-union-class-name/index.js
+++ b/src/rules/selector-no-union-class-name/index.js
@@ -70,7 +70,8 @@ function rule(actual) {
             result,
             node: rule,
             message: messages.rejected,
-            index: node.sourceIndex
+            index: node.sourceIndex,
+            endIndex: node.sourceIndex + rule.selector.length
           });
         });
       });


Test patch:
diff --git a/src/rules/at-import-partial-extension/__tests__/index.js b/src/rules/at-import-partial-extension/__tests__/index.js
index f1b6cd6..c8a9ee0 100644
--- a/src/rules/at-import-partial-extension/__tests__/index.js
+++ b/src/rules/at-import-partial-extension/__tests__/index.js
@@ -151,7 +151,9 @@ testRule({
       @import "fff";
     `,
       line: 2,
-      column: 7,
+      column: 16,
+      endLine: 2,
+      endColumn: 19,
       message: messages.expected,
       description: "Single file, no extension."
     },
@@ -161,7 +163,9 @@ testRule({
           "fff.ruthless";
     `,
       line: 2,
-      column: 7,
+      column: 16,
+      endLine: 2,
+      endColumn: 19,
       message: messages.expected,
       description: "Multiple files, one without extension."
     },
@@ -172,12 +176,16 @@ testRule({
       warnings: [
         {
           line: 2,
-          column: 7,
+          column: 16,
+          endLine: 2,
+          endColumn: 19,
           message: messages.expected
         },
         {
           line: 2,
-          column: 7,
+          column: 23,
+          endLine: 2,
+          endColumn: 28,
           message: messages.expected
         }
       ],
@@ -320,7 +328,9 @@ testRule({
       @import "fff";
      `,
       line: 2,
-      column: 20,
+      column: 19,
+      endLine: 2,
+      endColumn: 24,
       message: messages.rejected("scss"),
       description: "Single file, .scss extension."
     },
@@ -332,7 +342,9 @@ testRule({
       @import "screen";
     `,
       line: 2,
-      column: 23,
+      column: 22,
+      endLine: 2,
+      endColumn: 27,
       message: messages.rejected("scss"),
       description: "Single file with media query type as name, .scss extension."
     },
@@ -344,7 +356,9 @@ testRule({
       @import "fff ";
     `,
       line: 2,
-      column: 20,
+      column: 19,
+      endLine: 2,
+      endColumn: 24,
       message: messages.rejected("scss"),
       description: "Single file, has extension, space at the end."
     },
@@ -356,7 +370,9 @@ testRule({
       @import " fff ";
     `,
       line: 2,
-      column: 21,
+      column: 20,
+      endLine: 2,
+      endColumn: 25,
       message: messages.rejected("scss"),
       description: "Single file, has extension, trailing spaces."
     },
@@ -368,7 +384,9 @@ testRule({
       @import "df/fff";
     `,
       line: 2,
-      column: 23,
+      column: 22,
+      endLine: 2,
+      endColumn: 27,
       message: messages.rejected("scss"),
       description: "Single file, path with dir, has extension."
     },
@@ -380,7 +398,9 @@ testRule({
       @import "df\\fff";
     `,
       line: 2,
-      column: 23,
+      column: 22,
+      endLine: 2,
+      endColumn: 27,
       message: messages.rejected("scss"),
       description:
         "Single file, path with dir, has extension, windows delimiters."
@@ -393,7 +413,9 @@ testRule({
       @import "df/fff", '_1';
     `,
       line: 2,
-      column: 29,
+      column: 28,
+      endLine: 2,
+      endColumn: 33,
       message: messages.rejected("scss"),
       description: "Two files, path with dir, has extension."
     },
@@ -405,7 +427,9 @@ testRule({
       @import "colors.variables";
     `,
       line: 2,
-      column: 33,
+      column: 32,
+      endLine: 2,
+      endColumn: 37,
       message: messages.rejected("scss"),
       description: "Single file, has .scss extension and a dot in the name."
     },
@@ -417,7 +441,9 @@ testRule({
       @import "component.scss-theme";
     `,
       line: 2,
-      column: 26,
+      column: 36,
+      endLine: 2,
+      endColumn: 41,
       message: messages.rejected("scss"),
       description:
         "Single file, has .scss extension and a .scss in the filename."
diff --git a/src/rules/at-mixin-pattern/__tests__/index.js b/src/rules/at-mixin-pattern/__tests__/index.js
index 7c7f543..bf8847c 100644
--- a/src/rules/at-mixin-pattern/__tests__/index.js
+++ b/src/rules/at-mixin-pattern/__tests__/index.js
@@ -77,9 +77,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description: "Regexp: sequence part. Example: symbol in between."
     }
@@ -123,9 +123,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description: "String: sequence part. Example: symbol in between."
     },
@@ -135,9 +135,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 16,
       message: messages.expected,
       description: "String: sequence part. Example: not a full sequence."
     }
@@ -209,9 +209,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description: "Regexp: strict match. Example: matches at the end."
     },
@@ -221,9 +221,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description: "Regexp: strict match. Example: matches at the beginning."
     },
@@ -233,9 +233,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description: "Regexp: strict match. Example: symbol in between."
     },
@@ -247,9 +247,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
+      column: 14,
       endLine: 3,
-      endColumn: 0,
+      endColumn: 10,
       message: messages.expected,
       description:
         "Regexp: strict match. Example: mixin name divided by newlines."
@@ -288,9 +288,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description:
         "Regexp: pattern at the beginning. Example: matches at the end."
@@ -301,9 +301,9 @@ testRule({
       }
     `,
       line: 2,
-      column: 7,
-      endLine: 3,
-      endColumn: 0,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description:
         "Regexp: pattern at the beginning. Example: symbol in between."
@@ -332,9 +332,9 @@ testRule({
     {
       code: "@mixin boo-Foo-bar ( $p) {}",
       line: 1,
-      column: 1,
-      endLine: 2,
-      endColumn: 0,
+      column: 8,
+      endLine: 1,
+      endColumn: 19,
       message: messages.expected,
       description:
         "Regexp: SUIT component. Example: starts with lowercase, two elements"
@@ -342,18 +342,18 @@ testRule({
     {
       code: "@mixin foo-bar ($p) {}",
       line: 1,
-      column: 1,
-      endLine: 2,
-      endColumn: 0,
+      column: 8,
+      endLine: 1,
+      endColumn: 15,
       message: messages.expected,
       description: "Regexp: SUIT component. Example: starts with lowercase"
     },
     {
       code: "@mixin Foo-Bar ($p) {}",
       line: 1,
-      column: 1,
-      endLine: 2,
-      endColumn: 0,
+      column: 8,
+      endLine: 1,
+      endColumn: 15,
       message: messages.expected,
       description:
         "Regexp: SUIT component. Example: element starts with uppercase"
diff --git a/src/rules/at-root-no-redundant/__tests__/index.js b/src/rules/at-root-no-redundant/__tests__/index.js
index d368081..ed7a6c8 100644
--- a/src/rules/at-root-no-redundant/__tests__/index.js
+++ b/src/rules/at-root-no-redundant/__tests__/index.js
@@ -89,6 +89,8 @@ testRule({
       `,
       line: 2,
       column: 7,
+      endLine: 2,
+      endColumn: 15,
       message: messages.rejected,
       description: "@at-root rule is already in the root."
     },
diff --git a/src/rules/at-use-no-redundant-alias/__tests__/index.js b/src/rules/at-use-no-redundant-alias/__tests__/index.js
index 21bc0d2..e54dab2 100644
--- a/src/rules/at-use-no-redundant-alias/__tests__/index.js
+++ b/src/rules/at-use-no-redundant-alias/__tests__/index.js
@@ -40,7 +40,9 @@ testRule({
       @use "foo" as foo;
     `,
       line: 2,
-      column: 7,
+      column: 18,
+      endLine: 2,
+      endColumn: 24,
       message: messages.rejected,
       description: "Without url"
     },
@@ -49,7 +51,9 @@ testRule({
       @use "src/corners" as corners;
     `,
       line: 2,
-      column: 7,
+      column: 26,
+      endLine: 2,
+      endColumn: 36,
       message: messages.rejected,
       description: "With url"
     },
@@ -58,7 +62,9 @@ testRule({
       @use "sass:math" as math;
     `,
       line: 2,
-      column: 7,
+      column: 24,
+      endLine: 2,
+      endColumn: 31,
       message: messages.rejected,
       description: "Built-in module"
     },
@@ -67,7 +73,9 @@ testRule({
       @use "foo"   as   foo;
     `,
       line: 2,
-      column: 7,
+      column: 20,
+      endLine: 2,
+      endColumn: 28,
       message: messages.rejected,
       description: "Without space"
     },
@@ -76,7 +84,9 @@ testRule({
       @use "foo" as foo with ($baz: 1px);
     `,
       line: 2,
-      column: 7,
+      column: 18,
+      endLine: 2,
+      endColumn: 24,
       message: messages.rejected,
       description: "Configured"
     },
@@ -87,7 +97,9 @@ testRule({
       );
     `,
       line: 2,
-      column: 7,
+      column: 25,
+      endLine: 2,
+      endColumn: 33,
       message: messages.rejected,
       description: "Configured multiline"
     }
@@ -114,7 +126,9 @@ testRule({
       code: `@use "foo" as foo;`,
       fixed: `@use "foo";`,
       line: 1,
-      column: 1,
+      column: 12,
+      endLine: 1,
+      endColumn: 18,
       message: messages.rejected,
       description: "Without url"
     },
@@ -122,7 +136,9 @@ testRule({
       code: `@use "src/corners" as corners;`,
       fixed: `@use "src/corners";`,
       line: 1,
-      column: 1,
+      column: 20,
+      endLine: 1,
+      endColumn: 30,
       message: messages.rejected,
       description: "With url"
     },
@@ -130,7 +146,9 @@ testRule({
       code: `@use "sass:math" as math;`,
       fixed: `@use "sass:math";`,
       line: 1,
-      column: 1,
+      column: 18,
+      endLine: 1,
+      endColumn: 25,
       message: messages.rejected,
       description: "Built-in module"
     },
@@ -138,7 +156,9 @@ testRule({
       code: `@use "foo"   as   foo;`,
       fixed: `@use "foo";`,
       line: 1,
-      column: 1,
+      column: 14,
+      endLine: 1,
+      endColumn: 22,
       message: messages.rejected,
       description: "Without space"
     },
@@ -146,7 +166,9 @@ testRule({
       code: `@use "foo" as foo with ($baz: 1px);`,
       fixed: `@use "foo" with ($baz: 1px);`,
       line: 1,
-      column: 1,
+      column: 12,
+      endLine: 1,
+      endColumn: 18,
       message: messages.rejected,
       description: "Configured"
     },
@@ -162,7 +184,9 @@ testRule({
       );
     `,
       line: 2,
-      column: 7,
+      column: 25,
+      endLine: 2,
+      endColumn: 33,
       message: messages.rejected,
       description: "Configured multiline"
     }
diff --git a/src/rules/at-use-no-unnamespaced/__tests__/index.js b/src/rules/at-use-no-unnamespaced/__tests__/index.js
index db9fbb8..283dc70 100644
--- a/src/rules/at-use-no-unnamespaced/__tests__/index.js
+++ b/src/rules/at-use-no-unnamespaced/__tests__/index.js
@@ -28,7 +28,9 @@ testRule({
       @use "foo" as *;
     `,
       line: 2,
-      column: 7,
+      column: 18,
+      endLine: 2,
+      endColumn: 22,
       message: messages.rejected,
       description: "Without namespace"
     },
@@ -37,7 +39,9 @@ testRule({
       @use "foo"as*;
     `,
       line: 2,
-      column: 7,
+      column: 17,
+      endLine: 2,
+      endColumn: 20,
       message: messages.rejected,
       description: "Without space"
     },
@@ -46,7 +50,9 @@ testRule({
       @use "foo"   as   *  ;
     `,
       line: 2,
-      column: 7,
+      column: 20,
+      endLine: 2,
+      endColumn: 26,
       message: messages.rejected,
       description: "With many spaces"
     },
@@ -55,7 +61,9 @@ testRule({
       @use "foo" as * with ($baz: 1px);
     `,
       line: 2,
-      column: 7,
+      column: 18,
+      endLine: 2,
+      endColumn: 22,
       message: messages.rejected,
       description: "Configured"
     },
@@ -66,7 +74,9 @@ testRule({
       );
     `,
       line: 2,
-      column: 7,
+      column: 18,
+      endLine: 2,
+      endColumn: 22,
       message: messages.rejected,
       description: "Configured multiline"
     }
diff --git a/src/rules/dimension-no-non-numeric-values/__tests__/index.js b/src/rules/dimension-no-non-numeric-values/__tests__/index.js
index c6a1ac4..048eadc 100644
--- a/src/rules/dimension-no-non-numeric-values/__tests__/index.js
+++ b/src/rules/dimension-no-non-numeric-values/__tests__/index.js
@@ -43,14 +43,18 @@ testRule({
       `,
     message: messages.rejected("%unit%"),
     line: 3,
-    column: 27,
+    column: 18,
+    endLine: 3,
+    endColumn: 33,
     description: "Rejects interpolation with %unit%"
   }).concat([
     {
       code: "$pad: 2; $padAndMore: #{$pad + 5}px;",
       description: "reports lint when expression used in interpolation",
       line: 1,
-      column: 34,
+      column: 23,
+      endLine: 1,
+      endColumn: 36,
       message: messages.rejected("px")
     },
     {
@@ -61,7 +65,9 @@ testRule({
       `,
       description: "reports lint when mixing accepted and rejected syntax",
       line: 3,
-      column: 39,
+      column: 32,
+      endLine: 3,
+      endColumn: 41,
       message: messages.rejected("px")
     },
     {
@@ -72,7 +78,9 @@ testRule({
       `,
       description: "reports lint when mixing accepted and rejected syntax",
       line: 3,
-      column: 39,
+      column: 32,
+      endLine: 3,
+      endColumn: 41,
       message: messages.rejected("px")
     },
     {
@@ -83,7 +91,9 @@ testRule({
       `,
       description: "reports lint when mixing normal unit and rejected syntax",
       line: 3,
-      column: 29,
+      column: 22,
+      endLine: 3,
+      endColumn: 33,
       message: messages.rejected("vmin")
     },
     {
@@ -94,7 +104,9 @@ testRule({
       `,
       description: "reports lint when mixing normal unit and rejected syntax",
       line: 3,
-      column: 29,
+      column: 22,
+      endLine: 3,
+      endColumn: 33,
       message: messages.rejected("vmin")
     }
   ])
diff --git a/src/rules/dollar-variable-no-missing-interpolation/__tests__/index.js b/src/rules/dollar-variable-no-missing-interpolation/__tests__/index.js
index eabc837..e505f35 100644
--- a/src/rules/dollar-variable-no-missing-interpolation/__tests__/index.js
+++ b/src/rules/dollar-variable-no-missing-interpolation/__tests__/index.js
@@ -170,6 +170,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 25,
+      endLine: 4,
+      endColumn: 29,
       message: messages.rejected("animation-name", "$var"),
       description:
         "when variable is a string and it is not interpolated in animation-name property"
@@ -207,6 +210,9 @@ testRule({
       @keyframes $var {}
     `,
       line: 4,
+      column: 18,
+      endLine: 4,
+      endColumn: 22,
       message: messages.rejected("@keyframes", "$var"),
       description: "when variable is not a string and it is used in @keyframes"
     },
@@ -244,6 +250,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 22,
+      endLine: 4,
+      endColumn: 26,
       message: messages.rejected("@counter-style", "$var"),
       description: "when variable is a string and it is used in @counter-style"
     },
@@ -272,6 +281,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 34,
+      endLine: 4,
+      endColumn: 38,
       message: messages.rejected("@supports", "$var"),
       description: "when variable is a string and it is used in @supports"
     },
diff --git a/src/rules/dollar-variable-pattern/__tests__/index.js b/src/rules/dollar-variable-pattern/__tests__/index.js
index 5400a4c..a997d8d 100644
--- a/src/rules/dollar-variable-pattern/__tests__/index.js
+++ b/src/rules/dollar-variable-pattern/__tests__/index.js
@@ -43,6 +43,9 @@ testRule({
       }
     `,
       line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 14,
       message: messages.expected,
       description: "Regexp: sequence part. Example: symbol in between."
     }
diff --git a/src/rules/function-calculation-no-interpolation/__tests__/index.js b/src/rules/function-calculation-no-interpolation/__tests__/index.js
index fa91900..e7c233d 100644
--- a/src/rules/function-calculation-no-interpolation/__tests__/index.js
+++ b/src/rules/function-calculation-no-interpolation/__tests__/index.js
@@ -34,17 +34,22 @@ testRule({
       .a { .b: calc(#{$c} + 1); }
       `,
       line: 3,
-      column: 12,
+      column: 21,
+      endLine: 3,
+      endColumn: 26,
       message: messages.rejected("calc"),
       description: "`calc` function one argument interpolated"
     },
     {
+      only: true,
       code: `
       $c: 1;
       .a { .b: calc(#{$c + 1}); }
       `,
       line: 3,
-      column: 12,
+      column: 21,
+      endLine: 3,
+      endColumn: 25,
       message: messages.rejected("calc"),
       description: "`calc` function all arguments interpolated"
     },
@@ -54,7 +59,9 @@ testRule({
       .a { .b: calc(max(#{$c})); }
       `,
       line: 3,
-      column: 12,
+      column: 25,
+      endLine: 3,
+      endColumn: 30,
       message: messages.rejected("max"),
       description: "`max` function with interpolation"
     },
@@ -64,7 +71,9 @@ testRule({
       .a { .b: min(#{$c} + 1px); }
       `,
       line: 3,
-      column: 12,
+      column: 20,
+      endLine: 3,
+      endColumn: 25,
       message: messages.rejected("min"),
       description: "`min` function with interpolation"
     },
@@ -74,7 +83,9 @@ testRule({
       .a { .b: clamp(#{$c} + #{$d}); }
       `,
       line: 3,
-      column: 12,
+      column: 22,
+      endLine: 3,
+      endColumn: 27,
       message: messages.rejected("clamp"),
       description: "`clamp` function with interpolation"
     }
diff --git a/src/rules/function-color-relative/__tests__/index.js b/src/rules/function-color-relative/__tests__/index.js
index 671754d..24e9da6 100644
--- a/src/rules/function-color-relative/__tests__/index.js
+++ b/src/rules/function-color-relative/__tests__/index.js
@@ -44,7 +44,9 @@ testRule({
       description: "does not accept the saturate function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 26
     },
     {
       code: `
@@ -55,7 +57,9 @@ testRule({
       description: "does not accept the desaturate function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 28
     },
     {
       code: `
@@ -66,7 +70,9 @@ testRule({
       description: "does not accept the darken function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 24
     },
     {
       code: `
@@ -77,7 +83,9 @@ testRule({
       description: "does not accept the lighten function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 25
     },
     {
       code: `
@@ -88,7 +96,9 @@ testRule({
       description: "does not accept the opacify function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 25
     },
     {
       code: `
@@ -99,7 +109,9 @@ testRule({
       description: "does not accept the fade-in function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 25
     },
     {
       code: `
@@ -110,7 +122,9 @@ testRule({
       description: "does not accept the transparentize function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 32
     },
     {
       code: `
@@ -121,7 +135,9 @@ testRule({
       description: "does not accept the fade-out function",
       message: messages.rejected,
       line: 3,
-      column: 18
+      column: 18,
+      endLine: 3,
+      endColumn: 26
     },
     {
       code: `
@@ -133,7 +149,9 @@ testRule({
         "does not accept color functions inside a drop-shadow filter",
       message: messages.rejected,
       line: 3,
-      column: 39
+      column: 39,
+      endLine: 3,
+      endColumn: 47
     },
     {
       code: `
@@ -145,7 +163,9 @@ testRule({
         "does not accept color functions inside a drop-shadow filter when multiple filters are used",
       message: messages.rejected,
       line: 3,
-      column: 54
+      column: 54,
+      endLine: 3,
+      endColumn: 62
     },
     {
       code: `
@@ -157,7 +177,9 @@ testRule({
         "does not accept color functions inside a drop-shadow filter when multiple filters are used",
       message: messages.rejected,
       line: 3,
-      column: 53
+      column: 53,
+      endLine: 3,
+      endColumn: 61
     },
     {
       code: `
@@ -169,7 +191,9 @@ testRule({
         "does not accept color functions inside a drop-shadow filter when multiple filters are used",
       message: messages.rejected,
       line: 3,
-      column: 39
+      column: 39,
+      endLine: 3,
+      endColumn: 47
     }
   ]
 });
diff --git a/src/rules/function-disallowed-list/__tests__/index.js b/src/rules/function-disallowed-list/__tests__/index.js
index 8913524..4d63d46 100644
--- a/src/rules/function-disallowed-list/__tests__/index.js
+++ b/src/rules/function-disallowed-list/__tests__/index.js
@@ -24,7 +24,9 @@ testRule({
             .a { margin-left: math.random(100); }
           `,
       line: 3,
-      column: 18,
+      column: 36,
+      endLine: 3,
+      endColumn: 42,
       message: messages.rejected("random"),
       description: "Math library function, not allowed."
     }
@@ -60,7 +62,9 @@ testRule({
           .a { margin-left: math.random(100); }
         `,
       line: 3,
-      column: 16,
+      column: 34,
+      endLine: 3,
+      endColumn: 40,
       message: messages.rejected("random"),
       description: "Math library function, not allowed."
     },
@@ -70,7 +74,9 @@ testRule({
         .a { margin-left: test(3); }
       `,
       line: 3,
-      column: 14,
+      column: 27,
+      endLine: 3,
+      endColumn: 31,
       message: messages.rejected("test"),
       description: "Declared function is not allowed (regex)."
     },
@@ -80,7 +86,9 @@ testRule({
         .a { margin-left: random(5); }
       `,
       line: 3,
-      column: 14,
+      column: 27,
+      endLine: 3,
+      endColumn: 33,
       message: messages.rejected("random"),
       description: "Declared function is not allowed."
     }
diff --git a/src/rules/load-no-partial-leading-underscore/__tests__/index.js b/src/rules/load-no-partial-leading-underscore/__tests__/index.js
index a50042a..2d5f519 100644
--- a/src/rules/load-no-partial-leading-underscore/__tests__/index.js
+++ b/src/rules/load-no-partial-leading-underscore/__tests__/index.js
@@ -143,6 +143,9 @@ testRule({
       @import "_fff";
     `,
       line: 2,
+      column: 16,
+      endLine: 2,
+      endColumn: 20,
       message: messages.expected,
       description: "@import one file, underscore at the start."
     },
@@ -151,6 +154,9 @@ testRule({
       @import "_fff ";
     `,
       line: 2,
+      column: 16,
+      endLine: 2,
+      endColumn: 20,
       message: messages.expected,
       description:
         "@import one file, underscore at the start, trailing space at the end."
@@ -160,6 +166,9 @@ testRule({
       @import " _fff";
     `,
       line: 2,
+      column: 17,
+      endLine: 2,
+      endColumn: 21,
       message: messages.expected,
       description:
         "@import one file, underscore at the start, trailing space at the start."
@@ -169,6 +178,9 @@ testRule({
       @import "df/_fff";
     `,
       line: 2,
+      column: 16,
+      endLine: 2,
+      endColumn: 23,
       message: messages.expected,
       description: "@import one file, path with dir, underscore at the start."
     },
@@ -177,6 +189,9 @@ testRule({
       @import "df\\_fff";
     `,
       line: 2,
+      column: 16,
+      endLine: 2,
+      endColumn: 23,
       message: messages.expected,
       description: "@import one file, path with dir, windows delimiters."
     },
@@ -185,6 +200,9 @@ testRule({
       @import "df/fff", '_1.scss';
     `,
       line: 2,
+      column: 26,
+      endLine: 2,
+      endColumn: 33,
       message: messages.expected,
       description: "@import two files, path with dir, underscore at the start."
     },
@@ -193,6 +211,9 @@ testRule({
       @use "_fff";
     `,
       line: 2,
+      column: 13,
+      endLine: 2,
+      endColumn: 17,
       message: messages.expected,
       description: "@use one file, underscore at the start."
     },
@@ -201,6 +222,9 @@ testRule({
       @use "_fff ";
     `,
       line: 2,
+      column: 13,
+      endLine: 2,
+      endColumn: 17,
       message: messages.expected,
       description:
         "@use one file, underscore at the start, trailing space at the end."
@@ -210,6 +234,9 @@ testRule({
       @use " _fff";
     `,
       line: 2,
+      column: 14,
+      endLine: 2,
+      endColumn: 18,
       message: messages.expected,
       description:
         "@use one file, underscore at the start, trailing space at the start."
@@ -219,6 +246,9 @@ testRule({
       @use "df/_fff";
     `,
       line: 2,
+      column: 13,
+      endLine: 2,
+      endColumn: 20,
       message: messages.expected,
       description: "@use one file, path with dir, underscore at the start."
     },
@@ -227,6 +257,9 @@ testRule({
       @use "df\\_fff";
     `,
       line: 2,
+      column: 13,
+      endLine: 2,
+      endColumn: 20,
       message: messages.expected,
       description: "@use one file, path with dir, windows delimiters."
     },
@@ -235,6 +268,9 @@ testRule({
       @use "df/fff", '_1.scss';
     `,
       line: 2,
+      column: 23,
+      endLine: 2,
+      endColumn: 30,
       message: messages.expected,
       description: "@use two files, path with dir, underscore at the start."
     },
@@ -243,6 +279,9 @@ testRule({
       @forward "_fff";
     `,
       line: 2,
+      column: 17,
+      endLine: 2,
+      endColumn: 21,
       message: messages.expected,
       description: "@forward one file, underscore at the start."
     },
@@ -251,6 +290,9 @@ testRule({
       @forward "_fff ";
     `,
       line: 2,
+      column: 17,
+      endLine: 2,
+      endColumn: 21,
       message: messages.expected,
       description:
         "@forward one file, underscore at the start, trailing space at the end."
@@ -260,6 +302,9 @@ testRule({
       @forward " _fff";
     `,
       line: 2,
+      column: 18,
+      endLine: 2,
+      endColumn: 22,
       message: messages.expected,
       description:
         "@forward one file, underscore at the start, trailing space at the start."
@@ -269,6 +314,9 @@ testRule({
       @forward "df/_fff";
     `,
       line: 2,
+      column: 17,
+      endLine: 2,
+      endColumn: 24,
       message: messages.expected,
       description: "@forward one file, path with dir, underscore at the start."
     },
@@ -277,6 +325,9 @@ testRule({
       @forward "df\\_fff";
     `,
       line: 2,
+      column: 17,
+      endLine: 2,
+      endColumn: 24,
       message: messages.expected,
       description: "@forward one file, path with dir, windows delimiters."
     },
@@ -285,6 +336,9 @@ testRule({
       @forward "df/fff", '_1.scss';
     `,
       line: 2,
+      column: 27,
+      endLine: 2,
+      endColumn: 34,
       message: messages.expected,
       description: "@forward two files, path with dir, underscore at the start."
     },
@@ -296,6 +350,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 33,
+      endLine: 4,
+      endColumn: 40,
       message: messages.expected,
       description:
         "meta.load-css two params, path with dir, windows delimiters."
@@ -308,6 +365,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 33,
+      endLine: 4,
+      endColumn: 37,
       message: messages.expected,
       description:
         "meta.load-css two params, underscore at the start, trailing space at the start."
@@ -320,6 +380,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 33,
+      endLine: 4,
+      endColumn: 37,
       message: messages.expected,
       description:
         "meta.load-css one param, underscore at the start, trailing space at the start."
diff --git a/src/rules/map-keys-quotes/__tests__/index.js b/src/rules/map-keys-quotes/__tests__/index.js
index bb3aabc..23ad29e 100644
--- a/src/rules/map-keys-quotes/__tests__/index.js
+++ b/src/rules/map-keys-quotes/__tests__/index.js
@@ -121,12 +121,16 @@ testRule({
       warnings: [
         {
           line: 2,
-          column: 9,
+          column: 17,
+          endLine: 2,
+          endColumn: 26,
           message: messages.expected
         },
         {
           line: 2,
-          column: 9,
+          column: 34,
+          endLine: 2,
+          endColumn: 39,
           message: messages.expected
         }
       ],
diff --git a/src/rules/no-dollar-variables/__tests__/index.js b/src/rules/no-dollar-variables/__tests__/index.js
index 3f9a645..6c96a79 100644
--- a/src/rules/no-dollar-variables/__tests__/index.js
+++ b/src/rules/no-dollar-variables/__tests__/index.js
@@ -28,6 +28,8 @@ testRule({
     `,
       line: 2,
       column: 7,
+      endLine: 2,
+      endColumn: 9,
       message: messages.rejected("$a"),
       description: "A dollar variable"
     },
diff --git a/src/rules/no-duplicate-dollar-variables/__tests__/index.js b/src/rules/no-duplicate-dollar-variables/__tests__/index.js
index 9bee6a8..2584afe 100644
--- a/src/rules/no-duplicate-dollar-variables/__tests__/index.js
+++ b/src/rules/no-duplicate-dollar-variables/__tests__/index.js
@@ -148,6 +148,8 @@ testRule({
     `,
       line: 3,
       column: 7,
+      endLine: 3,
+      endColumn: 9,
       message: messages.rejected("$a"),
       description: "Two dollar variables with the same name."
     },
diff --git a/src/rules/no-duplicate-mixins/__tests__/index.js b/src/rules/no-duplicate-mixins/__tests__/index.js
index 2b9dba5..c57a5e7 100644
--- a/src/rules/no-duplicate-mixins/__tests__/index.js
+++ b/src/rules/no-duplicate-mixins/__tests__/index.js
@@ -39,8 +39,10 @@ testRule({
         font-size: 18px;
       }
     `,
-      column: 7,
       line: 5,
+      column: 14,
+      endLine: 5,
+      endColumn: 31,
       message: messages.rejected("font-size-default"),
       description: "Two mixins with the same names."
     },
@@ -56,8 +58,10 @@ testRule({
         font-size: 18px;
       }
     `,
-      column: 7,
       line: 8,
+      column: 14,
+      endLine: 8,
+      endColumn: 31,
       message: messages.rejected("font-size-default"),
       description: "Three mixins including two with the same names."
     },
@@ -70,8 +74,10 @@ testRule({
         font-size: $var;
       }
     `,
-      column: 7,
       line: 5,
+      column: 14,
+      endLine: 5,
+      endColumn: 23,
       message: messages.rejected("font-size"),
       description:
         "Two mixins with the same names including one accepting arguments."
@@ -85,8 +91,10 @@ testRule({
         font-size: $var;
       }
     `,
-      column: 7,
       line: 5,
+      column: 14,
+      endLine: 5,
+      endColumn: 23,
       message: messages.rejected("font-size"),
       description: "Two mixins with the same names accepting arguments."
     },
@@ -103,8 +111,10 @@ testRule({
         @include font-size;
       }
     `,
-      column: 9,
       line: 7,
+      column: 16,
+      endLine: 7,
+      endColumn: 25,
       message: messages.rejected("font-size"),
       description: "Two mixins with the same names accepting arguments."
     }
diff --git a/src/rules/no-global-function-names/__tests__/index.js b/src/rules/no-global-function-names/__tests__/index.js
index 22fc9e9..5475203 100644
--- a/src/rules/no-global-function-names/__tests__/index.js
+++ b/src/rules/no-global-function-names/__tests__/index.js
@@ -302,6 +302,8 @@ testRule({
     `,
       line: 3,
       column: 21,
+      endLine: 3,
+      endColumn: 24,
       message: messages.rejected("red"),
       description: "red"
     },
@@ -471,7 +473,9 @@ testRule({
       }
     `,
       line: 3,
-      column: 21,
+      column: 23,
+      endLine: 3,
+      endColumn: 35,
       message: messages.rejected("change-color"),
       description: "change-color"
     },
diff --git a/src/rules/partial-no-import/__tests__/index.js b/src/rules/partial-no-import/__tests__/index.js
index 1fcd689..7bb32cd 100644
--- a/src/rules/partial-no-import/__tests__/index.js
+++ b/src/rules/partial-no-import/__tests__/index.js
@@ -3,6 +3,7 @@
 const path = require("path");
 const postcss = require("postcss");
 const rule = require("..");
+const { messages } = rule;
 
 function logError(err) {
   console.log(err.stack); // eslint-disable-line no-console
@@ -40,7 +41,7 @@ test("Import a file from non-partial .scss", done => {
 });
 
 test("Import a file from a partial .scss", done => {
-  expect.assertions(1);
+  expect.assertions(2);
   postcss([rule()])
     .process("@import 'file.scss';", {
       from: path.join(__dirname, "_test.scss")
@@ -49,6 +50,13 @@ test("Import a file from a partial .scss", done => {
       const warnings = result.warnings();
 
       expect(warnings).toHaveLength(1);
+      expect(warnings[0]).toMatchObject({
+        text: messages.expected,
+        line: 1,
+        column: 10,
+        endLine: 1,
+        endColumn: 19
+      });
       done();
     });
 });
diff --git a/src/rules/percent-placeholder-pattern/__tests__/index.js b/src/rules/percent-placeholder-pattern/__tests__/index.js
index 67a8ee5..7ebb030 100644
--- a/src/rules/percent-placeholder-pattern/__tests__/index.js
+++ b/src/rules/percent-placeholder-pattern/__tests__/index.js
@@ -40,6 +40,9 @@ testRule({
     {
       code: "%floo { top: 1em; }",
       line: 1,
+      column: 1,
+      endLine: 1,
+      endColumn: 6,
       message: messages.expected("floo"),
       description: "Regexp: sequence part. Example: symbol in between."
     }
@@ -113,6 +116,9 @@ testRule({
     {
       code: "% foo { top: 1em; }",
       line: 1,
+      column: 1,
+      endLine: 1,
+      endColumn: 2,
       message: messages.expected(""),
       description:
         "Regexp: strict match. Example: matches, but has a space after '%'."
@@ -223,7 +229,10 @@ testRule({
       a,
       &1oo {}
     }`,
-      line: 3,
+      line: 2,
+      column: 7,
+      endLine: 3,
+      endColumn: 14,
       message: messages.expected("Foo-bar1oo"),
       description:
         "Regexp: SUIT component. Example: nesting, only lv1 part comply."
@@ -235,6 +244,9 @@ testRule({
       }
     }`,
       line: 1,
+      column: 1,
+      endLine: 1,
+      endColumn: 4,
       message: messages.expected("Fo"),
       description:
         "Regexp: pattern at the beginning. Example: nesting, with nesting selector; first lv doesn't comply."
diff --git a/src/rules/selector-nest-combinators/__tests__/index.js b/src/rules/selector-nest-combinators/__tests__/index.js
index ba0a06d..11ca3a6 100644
--- a/src/rules/selector-nest-combinators/__tests__/index.js
+++ b/src/rules/selector-nest-combinators/__tests__/index.js
@@ -144,7 +144,9 @@ testRule({
       description: "when a child combinator is used instead of nesting",
       message: messages.expected(" ", "combinator"),
       line: 2,
-      column: 11
+      column: 11,
+      endLine: 2,
+      endColumn: 12
     },
     {
       code: `
@@ -153,7 +155,9 @@ testRule({
       description: "when a selector is chained with another",
       message: messages.expected("bar", "class"),
       line: 2,
-      column: 11
+      column: 11,
+      endLine: 2,
+      endColumn: 15
     },
     {
       code: `
@@ -163,7 +167,9 @@ testRule({
         "when a direct descendant combinator is used without nesting",
       message: messages.expected(">", "combinator"),
       line: 2,
-      column: 12
+      column: 11,
+      endLine: 2,
+      endColumn: 14
     },
     {
       code: `
@@ -172,7 +178,9 @@ testRule({
       description: "when pseudo classes are used without nesting",
       message: messages.expected(":hover", "pseudo"),
       line: 2,
-      column: 11
+      column: 11,
+      endLine: 2,
+      endColumn: 17
     },
     {
       code: `
@@ -181,7 +189,9 @@ testRule({
       description: "when universal selectors are used with a combinator",
       message: messages.expected("+", "combinator"),
       line: 2,
-      column: 9
+      column: 8,
+      endLine: 2,
+      endColumn: 11
     },
     {
       code: `
@@ -190,7 +200,9 @@ testRule({
       description: "when pseudo selectors only are chained",
       message: messages.expected(":last-child", "pseudo"),
       line: 2,
-      column: 25
+      column: 25,
+      endLine: 2,
+      endColumn: 36
     },
     {
       code: `
@@ -199,7 +211,9 @@ testRule({
       description: "when interpolation is used",
       message: messages.expectedInterpolation,
       line: 2,
-      column: 18
+      column: 18,
+      endLine: 2,
+      endColumn: 19
     }
   ]
 });
@@ -312,18 +326,11 @@ testRule({
       }
       `,
       description: "when child combinators are nested",
-      warnings: [
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        }
-      ]
+      message: messages.rejected,
+      line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 13
     },
     {
       code: `
@@ -332,23 +339,11 @@ testRule({
       }
       `,
       description: "when chained combinators are nested",
-      warnings: [
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 10
-        }
-      ]
+      message: messages.rejected,
+      line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 14
     },
     {
       code: `
@@ -357,28 +352,11 @@ testRule({
       }
       `,
       description: "when direct descendant combinators are nested",
-      warnings: [
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 11
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 13
-        }
-      ]
+      message: messages.rejected,
+      line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 16
     },
     {
       code: `
@@ -393,32 +371,16 @@ testRule({
         {
           message: messages.rejected,
           line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 10
+          column: 9,
+          endLine: 3,
+          endColumn: 13
         },
         {
           message: messages.rejected,
           line: 4,
-          column: 11
-        },
-        {
-          message: messages.rejected,
-          line: 4,
-          column: 11
-        },
-        {
-          message: messages.rejected,
-          line: 4,
-          column: 12
+          column: 11,
+          endLine: 4,
+          endColumn: 15
         }
       ]
     },
@@ -435,32 +397,16 @@ testRule({
         {
           message: messages.rejected,
           line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 10
-        },
-        {
-          message: messages.rejected,
-          line: 4,
-          column: 11
-        },
-        {
-          message: messages.rejected,
-          line: 4,
-          column: 11
+          column: 9,
+          endLine: 3,
+          endColumn: 15
         },
         {
           message: messages.rejected,
           line: 4,
-          column: 12
+          column: 11,
+          endLine: 4,
+          endColumn: 17
         }
       ]
     },
@@ -471,23 +417,11 @@ testRule({
       }
       `,
       description: "when pseudo classes are nested",
-      warnings: [
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 9
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 10
-        }
-      ]
+      message: messages.rejected,
+      line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 16
     },
     {
       code: `
@@ -498,18 +432,11 @@ testRule({
       }
       `,
       description: "when interpolation is used",
-      warnings: [
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 8
-        },
-        {
-          message: messages.rejected,
-          line: 3,
-          column: 8
-        }
-      ]
+      message: messages.rejected,
+      line: 3,
+      column: 8,
+      endLine: 3,
+      endColumn: 25
     }
   ]
 });
diff --git a/src/rules/selector-no-redundant-nesting-selector/__tests__/index.js b/src/rules/selector-no-redundant-nesting-selector/__tests__/index.js
index e8dd515..7b7a0fd 100644
--- a/src/rules/selector-no-redundant-nesting-selector/__tests__/index.js
+++ b/src/rules/selector-no-redundant-nesting-selector/__tests__/index.js
@@ -150,6 +150,9 @@ testRule({
       }
     `,
       line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 10,
       message: messages.rejected,
       description: "when an ampersand precedes a direct descendant operator"
     },
@@ -343,6 +346,9 @@ testRule({
       }
     `,
       line: 4,
+      column: 9,
+      endLine: 4,
+      endColumn: 10,
       message: messages.rejected,
       description:
         "when an ampersand is used in a comma sequence followed by a class"
@@ -363,11 +369,15 @@ testRule({
         {
           line: 4,
           column: 9,
+          endLine: 4,
+          endColumn: 10,
           message: messages.rejected
         },
         {
           line: 7,
           column: 9,
+          endLine: 7,
+          endColumn: 10,
           message: messages.rejected
         }
       ],
diff --git a/src/rules/selector-no-union-class-name/__tests__/index.js b/src/rules/selector-no-union-class-name/__tests__/index.js
index 0549f3b..bc5b085 100644
--- a/src/rules/selector-no-union-class-name/__tests__/index.js
+++ b/src/rules/selector-no-union-class-name/__tests__/index.js
@@ -122,6 +122,9 @@ testRule({
       }
     `,
       line: 3,
+      column: 9,
+      endLine: 3,
+      endColumn: 16,
       message: messages.rejected,
       description: "when an ampersand is chained with union class name (hyphen)"
     },