Repository: stylelint-scss/stylelint-scss
Pull request #602 (2022-05-09 15:20:38)

Add `meta` property for all rules
Each rule function should have properties like `ruleName`.
See https://stylelint.io/developer-guide/plugins

For example, `stylelint -f verbose` command can show rules' links:

<img width="739" alt="image" src="https://user-images.githubusercontent.com/473530/167443125-388b0c6f-63d3-4dc5-bcf7-1ea625c33fce.png">


kristerkari: Thanks! I'll try to get this reviewed ASAP.

Patch:
diff --git a/src/rules/at-each-key-value-single-line/index.js b/src/rules/at-each-key-value-single-line/index.js
index 139d6bd..7176c74 100644
--- a/src/rules/at-each-key-value-single-line/index.js
+++ b/src/rules/at-each-key-value-single-line/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { moduleNamespace, namespace } from "../../utils";
+import { moduleNamespace, namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-each-key-value-single-line");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
     "Use @each $key, $value in $map syntax instead of $value: map-get($map, $key)"
 });
 
-export default function(primary) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primary) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -68,6 +72,10 @@ export default function(primary) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 // Takes in a param string from node.params
 // Returns: [[key variable, value variable], map_decl] (all Strings)
 function separateEachParams(paramString) {
diff --git a/src/rules/at-else-closing-brace-newline-after/index.js b/src/rules/at-else-closing-brace-newline-after/index.js
index a768bde..59bd933 100644
--- a/src/rules/at-else-closing-brace-newline-after/index.js
+++ b/src/rules/at-else-closing-brace-newline-after/index.js
@@ -1,6 +1,7 @@
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 import { isBoolean } from "lodash";
+import { sassConditionalBraceNLAfterChecker } from "../at-if-closing-brace-newline-after";
 
 export const ruleName = namespace("at-else-closing-brace-newline-after");
 
@@ -9,9 +10,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: 'Unexpected newline after "}" of @else statement'
 });
 
-import { sassConditionalBraceNLAfterChecker } from "../at-if-closing-brace-newline-after";
+export const meta = {
+  url: ruleUrl(ruleName)
+};
 
-export default function(expectation, options, context) {
+export default function rule(expectation, options, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -45,3 +48,7 @@ export default function(expectation, options, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-else-closing-brace-space-after/index.js b/src/rules/at-else-closing-brace-space-after/index.js
index 6bf3022..6cff267 100644
--- a/src/rules/at-else-closing-brace-space-after/index.js
+++ b/src/rules/at-else-closing-brace-space-after/index.js
@@ -1,4 +1,4 @@
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 import { sassConditionalBraceSpaceAfterChecker } from "../at-if-closing-brace-space-after";
 
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: 'Unexpected space after "}" of @else statement'
 });
 
-export default function(expectation, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation,
@@ -31,3 +35,7 @@ export default function(expectation, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-else-empty-line-before/index.js b/src/rules/at-else-empty-line-before/index.js
index a7845c6..aabe972 100644
--- a/src/rules/at-else-empty-line-before/index.js
+++ b/src/rules/at-else-empty-line-before/index.js
@@ -1,4 +1,4 @@
-import { hasEmptyLine, namespace } from "../../utils";
+import { hasEmptyLine, namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 
 export const ruleName = namespace("at-else-empty-line-before");
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected empty line before @else"
 });
 
-export default function(expectation, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation,
@@ -45,3 +49,7 @@ export default function(expectation, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-else-if-parentheses-space-before/index.js b/src/rules/at-else-if-parentheses-space-before/index.js
index f9bc0f8..7af5c6b 100644
--- a/src/rules/at-else-if-parentheses-space-before/index.js
+++ b/src/rules/at-else-if-parentheses-space-before/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, whitespaceChecker } from "../../utils";
+import { namespace, ruleUrl, whitespaceChecker } from "../../utils";
 
 export const ruleName = namespace("at-else-if-parentheses-space-before");
 
@@ -10,7 +10,11 @@ export const messages = utils.ruleMessages(ruleName, {
     "Expected a single space before parentheses in else-if declaration"
 });
 
-export default function(value, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value,
@@ -44,3 +48,7 @@ export default function(value, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-extend-no-missing-placeholder/index.js b/src/rules/at-extend-no-missing-placeholder/index.js
index c003764..634af32 100644
--- a/src/rules/at-extend-no-missing-placeholder/index.js
+++ b/src/rules/at-extend-no-missing-placeholder/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-extend-no-missing-placeholder");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
     "Expected a placeholder selector (e.g. %placeholder) to be used in @extend"
 });
 
-export default function(actual) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
 
@@ -31,3 +35,7 @@ export default function(actual) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-function-named-arguments/index.js b/src/rules/at-function-named-arguments/index.js
index c402f20..a2d296f 100644
--- a/src/rules/at-function-named-arguments/index.js
+++ b/src/rules/at-function-named-arguments/index.js
@@ -5,7 +5,8 @@ import {
   isNativeCssFunction,
   namespace,
   optionsHaveIgnored,
-  parseFunctionArguments
+  parseFunctionArguments,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace("at-function-named-arguments");
@@ -15,9 +16,13 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected a named parameter in function call"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const isScssVarRegExp = /^\$\S*/;
 
-export default function(expectation, options) {
+export default function rule(expectation, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -116,3 +121,7 @@ export default function(expectation, options) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-function-parentheses-space-before/index.js b/src/rules/at-function-parentheses-space-before/index.js
index b2a6081..df4050b 100644
--- a/src/rules/at-function-parentheses-space-before/index.js
+++ b/src/rules/at-function-parentheses-space-before/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, whitespaceChecker } from "../../utils";
+import { namespace, ruleUrl, whitespaceChecker } from "../../utils";
 
 export const ruleName = namespace("at-function-parentheses-space-before");
 
@@ -10,7 +10,11 @@ export const messages = utils.ruleMessages(ruleName, {
     "Expected a single space before parentheses in function declaration"
 });
 
-export default function(value, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value,
@@ -41,3 +45,7 @@ export default function(value, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-function-pattern/index.js b/src/rules/at-function-pattern/index.js
index 2b719e2..68e33f8 100644
--- a/src/rules/at-function-pattern/index.js
+++ b/src/rules/at-function-pattern/index.js
@@ -1,6 +1,6 @@
 import { isRegExp, isString } from "lodash";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-function-pattern");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Expected @function name to match specified pattern"
 });
 
-export default function(pattern) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(pattern) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: pattern,
@@ -42,3 +46,7 @@ export default function(pattern) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-if-closing-brace-newline-after/index.js b/src/rules/at-if-closing-brace-newline-after/index.js
index 7f679d0..7e625d7 100644
--- a/src/rules/at-if-closing-brace-newline-after/index.js
+++ b/src/rules/at-if-closing-brace-newline-after/index.js
@@ -1,4 +1,4 @@
-import { isSingleLineString, namespace } from "../../utils";
+import { isSingleLineString, namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 import { isBoolean } from "lodash";
 
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: 'Unexpected newline after "}" of @if statement'
 });
 
-export default function(expectation, options, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, options, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -117,3 +121,7 @@ export function sassConditionalBraceNLAfterChecker({
     }
   });
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-if-closing-brace-space-after/index.js b/src/rules/at-if-closing-brace-space-after/index.js
index 5ef11d2..58becb9 100644
--- a/src/rules/at-if-closing-brace-space-after/index.js
+++ b/src/rules/at-if-closing-brace-space-after/index.js
@@ -1,4 +1,4 @@
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 
 export const ruleName = namespace("at-if-closing-brace-space-after");
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: 'Unexpected space after "}" of @if statement'
 });
 
-export default function(expectation, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation,
@@ -91,3 +95,7 @@ export function sassConditionalBraceSpaceAfterChecker({
     }
   });
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-if-no-null/index.js b/src/rules/at-if-no-null/index.js
index 693c412..ac9e875 100644
--- a/src/rules/at-if-no-null/index.js
+++ b/src/rules/at-if-no-null/index.js
@@ -1,4 +1,4 @@
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 
 export const ruleName = namespace("at-if-no-null");
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   not_equals_null: "Expected @if statement rather than @if statement != null"
 });
 
-export default function(expectation) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation
@@ -47,3 +51,7 @@ export default function(expectation) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-import-no-partial-leading-underscore/index.js b/src/rules/at-import-no-partial-leading-underscore/index.js
index c900c03..463d292 100644
--- a/src/rules/at-import-no-partial-leading-underscore/index.js
+++ b/src/rules/at-import-no-partial-leading-underscore/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-import-no-partial-leading-underscore");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Unexpected leading underscore in imported partial name"
 });
 
-export default function(actual) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
 
@@ -52,3 +56,7 @@ export default function(actual) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-import-partial-extension-blacklist/index.js b/src/rules/at-import-partial-extension-blacklist/index.js
index dc1adc1..160b4a5 100644
--- a/src/rules/at-import-partial-extension-blacklist/index.js
+++ b/src/rules/at-import-partial-extension-blacklist/index.js
@@ -1,6 +1,6 @@
 import { isRegExp, isString } from "lodash";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import nodeJsPath from "path";
 
 export const ruleName = namespace("at-import-partial-extension-blacklist");
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: ext => `Unexpected extension ".${ext}" in imported partial name`
 });
 
-export default function(blacklistOption) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(blacklistOption) {
   const blacklist = [].concat(blacklistOption);
 
   return (root, result) => {
@@ -70,3 +74,7 @@ export default function(blacklistOption) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-import-partial-extension-whitelist/index.js b/src/rules/at-import-partial-extension-whitelist/index.js
index 5146031..873b22d 100644
--- a/src/rules/at-import-partial-extension-whitelist/index.js
+++ b/src/rules/at-import-partial-extension-whitelist/index.js
@@ -1,6 +1,6 @@
 import { isRegExp, isString } from "lodash";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import nodeJsPath from "path";
 
 export const ruleName = namespace("at-import-partial-extension-whitelist");
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: ext => `Unexpected extension ".${ext}" in imported partial name`
 });
 
-export default function(whitelistOption) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(whitelistOption) {
   const whitelist = [].concat(whitelistOption);
 
   return (root, result) => {
@@ -74,3 +78,7 @@ export default function(whitelistOption) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-import-partial-extension/index.js b/src/rules/at-import-partial-extension/index.js
index 9a5db91..399c7f6 100644
--- a/src/rules/at-import-partial-extension/index.js
+++ b/src/rules/at-import-partial-extension/index.js
@@ -1,6 +1,6 @@
 import nodeJsPath from "path";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-import-partial-extension");
 
@@ -9,6 +9,10 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: ext => `Unexpected extension ".${ext}" in @import`
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 // https://drafts.csswg.org/mediaqueries/#media-types
 const mediaQueryTypes = [
   "all",
@@ -28,7 +32,7 @@ const mediaQueryTypesRE = new RegExp(`(${mediaQueryTypes.join("|")})$`, "i");
 const stripPath = path =>
   path.replace(/^\s*(["'])\s*/, "").replace(/\s*(["'])\s*$/, "");
 
-export default function(expectation, _, context) {
+export default function rule(expectation, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation,
@@ -91,3 +95,7 @@ export default function(expectation, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-mixin-argumentless-call-parentheses/index.js b/src/rules/at-mixin-argumentless-call-parentheses/index.js
index bb036f7..6a98a6e 100644
--- a/src/rules/at-mixin-argumentless-call-parentheses/index.js
+++ b/src/rules/at-mixin-argumentless-call-parentheses/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-mixin-argumentless-call-parentheses");
 
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
     `Unexpected parentheses in argumentless mixin "${mixin}" call`
 });
 
-export default function(value, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value,
@@ -54,3 +58,7 @@ export default function(value, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-mixin-named-arguments/index.js b/src/rules/at-mixin-named-arguments/index.js
index 1a404bb..6b7ed22 100644
--- a/src/rules/at-mixin-named-arguments/index.js
+++ b/src/rules/at-mixin-named-arguments/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, optionsHaveIgnored } from "../../utils";
+import { namespace, optionsHaveIgnored, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-mixin-named-arguments");
 
@@ -8,10 +8,14 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected a named parameter in at-include call"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const hasArgumentsRegExp = /\((.*)\)$/;
 const isScssVarRegExp = /^\$\S*/;
 
-export default function(expectation, options) {
+export default function rule(expectation, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -109,3 +113,7 @@ export default function(expectation, options) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-mixin-parentheses-space-before/index.js b/src/rules/at-mixin-parentheses-space-before/index.js
index 0538549..ec5246b 100644
--- a/src/rules/at-mixin-parentheses-space-before/index.js
+++ b/src/rules/at-mixin-parentheses-space-before/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, whitespaceChecker } from "../../utils";
+import { namespace, ruleUrl, whitespaceChecker } from "../../utils";
 
 export const ruleName = namespace("at-mixin-parentheses-space-before");
 
@@ -10,7 +10,11 @@ export const messages = utils.ruleMessages(ruleName, {
     "Expected a single space before parentheses in mixin declaration"
 });
 
-export default function(value, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value,
@@ -41,3 +45,7 @@ export default function(value, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-mixin-pattern/index.js b/src/rules/at-mixin-pattern/index.js
index d634e98..f054707 100644
--- a/src/rules/at-mixin-pattern/index.js
+++ b/src/rules/at-mixin-pattern/index.js
@@ -1,6 +1,6 @@
 import { isRegExp, isString } from "lodash";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-mixin-pattern");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Expected @mixin name to match specified pattern"
 });
 
-export default function(pattern) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(pattern) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: pattern,
@@ -42,3 +46,7 @@ export default function(pattern) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-rule-conditional-no-parentheses/index.js b/src/rules/at-rule-conditional-no-parentheses/index.js
index 08978d8..280efb0 100644
--- a/src/rules/at-rule-conditional-no-parentheses/index.js
+++ b/src/rules/at-rule-conditional-no-parentheses/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-rule-conditional-no-parentheses");
 
@@ -7,6 +7,10 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected () used to surround statements for @-rules"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 // postcss picks up else-if as else.
 const conditional_rules = ["if", "while", "else"];
 
@@ -28,7 +32,7 @@ function fix(atrule) {
   atrule.params = [...new Set(groups)].join(" ");
 }
 
-export default function(primary, _unused, context) {
+export default function rule(primary, _unused, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -67,3 +71,7 @@ export default function(primary, _unused, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-rule-no-unknown/index.js b/src/rules/at-rule-no-unknown/index.js
index 74f2ec5..ef0634c 100644
--- a/src/rules/at-rule-no-unknown/index.js
+++ b/src/rules/at-rule-no-unknown/index.js
@@ -1,6 +1,6 @@
 import { isRegExp, isString } from "lodash";
 import { rules, utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 const sassAtRules = [
   "at-root",
@@ -37,7 +37,11 @@ export const messages = utils.ruleMessages(ruleName, {
   }
 });
 
-export default function(primaryOption, secondaryOptions) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primaryOption, secondaryOptions) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -87,3 +91,7 @@ export default function(primaryOption, secondaryOptions) {
     );
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/at-use-no-unnamespaced/index.js b/src/rules/at-use-no-unnamespaced/index.js
index e3781e8..d60ed48 100644
--- a/src/rules/at-use-no-unnamespaced/index.js
+++ b/src/rules/at-use-no-unnamespaced/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("at-use-no-unnamespaced");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected @use without namespace"
 });
 
-export default function(actual) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
 
@@ -27,3 +31,7 @@ export default function(actual) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/comment-no-empty/index.js b/src/rules/comment-no-empty/index.js
index 0243a31..d2a6e33 100644
--- a/src/rules/comment-no-empty/index.js
+++ b/src/rules/comment-no-empty/index.js
@@ -1,5 +1,5 @@
 import { rules, utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 const coreRuleName = "comment-no-empty";
 
@@ -12,7 +12,11 @@ export const messages = utils.ruleMessages(ruleName, {
   )
 });
 
-function rule(primary) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primary) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -35,8 +39,10 @@ function rule(primary) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 function isEmptyComment(comment) {
   return comment.text === "";
 }
-
-export default rule;
diff --git a/src/rules/comment-no-loud/index.js b/src/rules/comment-no-loud/index.js
index 00b8175..b703ab2 100644
--- a/src/rules/comment-no-loud/index.js
+++ b/src/rules/comment-no-loud/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("comment-no-loud");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Expected // for comments instead of /*"
 });
 
-function rule(primary) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primary) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -30,6 +34,10 @@ function rule(primary) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 function isLoudComment(comment) {
   const regex = new RegExp(/^[ \t\n]*\/\*/);
 
@@ -38,5 +46,3 @@ function isLoudComment(comment) {
 
   return regex.test(commentFirstLine);
 }
-
-export default rule;
diff --git a/src/rules/declaration-nested-properties-no-divided-groups/index.js b/src/rules/declaration-nested-properties-no-divided-groups/index.js
index af1ecb8..1d038f1 100644
--- a/src/rules/declaration-nested-properties-no-divided-groups/index.js
+++ b/src/rules/declaration-nested-properties-no-divided-groups/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, parseNestedPropRoot } from "../../utils";
+import { namespace, parseNestedPropRoot, ruleUrl } from "../../utils";
 
 const hasOwnProp = Object.prototype.hasOwnProperty;
 
@@ -12,7 +12,11 @@ export const messages = utils.ruleMessages(ruleName, {
     `Expected all nested properties of "${prop}" namespace to be in one nested group`
 });
 
-export default function(expectation) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation
@@ -66,3 +70,7 @@ export default function(expectation) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/declaration-nested-properties/index.js b/src/rules/declaration-nested-properties/index.js
index cfe5474..09b6467 100644
--- a/src/rules/declaration-nested-properties/index.js
+++ b/src/rules/declaration-nested-properties/index.js
@@ -3,7 +3,8 @@ import {
   isStandardSyntaxProperty,
   namespace,
   optionsHaveException,
-  parseNestedPropRoot
+  parseNestedPropRoot,
+  ruleUrl
 } from "../../utils";
 
 const hasOwnProp = Object.prototype.hasOwnProperty;
@@ -15,7 +16,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: prop => `Unexpected nested property "${prop}"`
 });
 
-export default function(expectation, options) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -74,7 +79,9 @@ export default function(expectation, options) {
           if (type === "rule" || (type === "decl" && decl.isNested)) {
             // `background:red {` - selector;
             // `background: red {` - nested prop; space is decisive here
-            const testForProp = parseNestedPropRoot(selector || decl.toString());
+            const testForProp = parseNestedPropRoot(
+              selector || decl.toString()
+            );
 
             if (testForProp && testForProp.propName !== undefined) {
               const ns = testForProp.propName.value;
@@ -153,3 +160,7 @@ export default function(expectation, options) {
     }
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dimension-no-non-numeric-values/index.js b/src/rules/dimension-no-non-numeric-values/index.js
index da40ff3..c2a90ee 100644
--- a/src/rules/dimension-no-non-numeric-values/index.js
+++ b/src/rules/dimension-no-non-numeric-values/index.js
@@ -1,6 +1,6 @@
 import valueParser from "postcss-value-parser";
 import { utils } from "stylelint";
-import { declarationValueIndex, namespace } from "../../utils";
+import { declarationValueIndex, namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("dimension-no-non-numeric-values");
 
@@ -9,6 +9,10 @@ export const messages = utils.ruleMessages(ruleName, {
     `Expected "$value * 1${unit}" instead of "#{$value}${unit}". Consider writing "value" in terms of ${unit} originally.`
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 export const units = [
   // Font-relative lengths:
   // https://www.w3.org/TR/css-values-4/#font-relative-lengths
@@ -113,6 +117,10 @@ export default function rule(primary) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 function isInterpolated(value) {
   let boolean = false;
 
diff --git a/src/rules/dollar-variable-colon-newline-after/index.js b/src/rules/dollar-variable-colon-newline-after/index.js
index b73cbb2..29608ec 100644
--- a/src/rules/dollar-variable-colon-newline-after/index.js
+++ b/src/rules/dollar-variable-colon-newline-after/index.js
@@ -4,6 +4,7 @@ import {
   declarationValueIndex,
   isSingleLineString,
   namespace,
+  ruleUrl,
   whitespaceChecker
 } from "../../utils";
 
@@ -15,7 +16,11 @@ export const messages = utils.ruleMessages(ruleName, {
     'Expected newline after ":" with a multi-line value'
 });
 
-export default function(expectation, options, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, options, context) {
   const checker = whitespaceChecker("newline", expectation, messages);
 
   return (root, result) => {
@@ -110,3 +115,7 @@ export default function(expectation, options, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-colon-space-after/index.js b/src/rules/dollar-variable-colon-space-after/index.js
index 5247a28..b974cd1 100644
--- a/src/rules/dollar-variable-colon-space-after/index.js
+++ b/src/rules/dollar-variable-colon-space-after/index.js
@@ -2,6 +2,7 @@ import {
   declarationValueIndex,
   namespace,
   isSingleLineString,
+  ruleUrl,
   whitespaceChecker
 } from "../../utils";
 import { utils } from "stylelint";
@@ -16,7 +17,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expectedAfterAtLeast: () => 'Expected at least one space after ":"'
 });
 
-export default function(expectation, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, _, context) {
   const checker = whitespaceChecker("space", expectation, messages);
 
   return (root, result) => {
@@ -41,6 +46,10 @@ export default function(expectation, _, context) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 export function variableColonSpaceChecker({
   locationChecker,
   root,
diff --git a/src/rules/dollar-variable-colon-space-before/index.js b/src/rules/dollar-variable-colon-space-before/index.js
index 56beb00..c09c2ad 100644
--- a/src/rules/dollar-variable-colon-space-before/index.js
+++ b/src/rules/dollar-variable-colon-space-before/index.js
@@ -1,4 +1,4 @@
-import { namespace, whitespaceChecker } from "../../utils";
+import { namespace, ruleUrl, whitespaceChecker } from "../../utils";
 import { utils } from "stylelint";
 import { variableColonSpaceChecker } from "../dollar-variable-colon-space-after";
 
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejectedBefore: () => 'Unexpected whitespace before ":"'
 });
 
-export default function(expectation, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, _, context) {
   const checker = whitespaceChecker("space", expectation, messages);
 
   return (root, result) => {
@@ -33,3 +37,7 @@ export default function(expectation, _, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-default/index.js b/src/rules/dollar-variable-default/index.js
index 5072f39..f122f3a 100644
--- a/src/rules/dollar-variable-default/index.js
+++ b/src/rules/dollar-variable-default/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, optionsHaveIgnored } from "../../utils";
+import { namespace, optionsHaveIgnored, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("dollar-variable-default");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: variable => `Expected !default flag for "${variable}"`
 });
 
-export default function(primaryOption, secondaryOptions) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primaryOption, secondaryOptions) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -55,3 +59,7 @@ export default function(primaryOption, secondaryOptions) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-empty-line-after/index.js b/src/rules/dollar-variable-empty-line-after/index.js
index 875891f..89ce15b 100644
--- a/src/rules/dollar-variable-empty-line-after/index.js
+++ b/src/rules/dollar-variable-empty-line-after/index.js
@@ -4,7 +4,8 @@ import {
   namespace,
   optionsHaveException,
   optionsHaveIgnored,
-  blockString
+  blockString,
+  ruleUrl
 } from "../../utils";
 import { utils } from "stylelint";
 import { isBoolean } from "lodash";
@@ -16,7 +17,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected empty line after $-variable"
 });
 
-export default function(expectation, options, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, options, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -238,3 +243,7 @@ export default function(expectation, options, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-empty-line-before/index.js b/src/rules/dollar-variable-empty-line-before/index.js
index ffff6ec..c0985e6 100644
--- a/src/rules/dollar-variable-empty-line-before/index.js
+++ b/src/rules/dollar-variable-empty-line-before/index.js
@@ -4,7 +4,8 @@ import {
   namespace,
   optionsHaveException,
   optionsHaveIgnored,
-  blockString
+  blockString,
+  ruleUrl
 } from "../../utils";
 import { utils } from "stylelint";
 import { isBoolean } from "lodash";
@@ -16,7 +17,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected empty line before $-variable"
 });
 
-export default function(expectation, options, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, options, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -147,6 +152,10 @@ export default function(expectation, options, context) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 function isDollarVar(node) {
   return node.prop && node.prop[0] === "$";
 }
diff --git a/src/rules/dollar-variable-first-in-block/index.js b/src/rules/dollar-variable-first-in-block/index.js
index d158108..93512e0 100644
--- a/src/rules/dollar-variable-first-in-block/index.js
+++ b/src/rules/dollar-variable-first-in-block/index.js
@@ -1,7 +1,8 @@
 import {
   namespace,
   optionsHaveException,
-  optionsHaveIgnored
+  optionsHaveIgnored,
+  ruleUrl
 } from "../../utils";
 import { utils } from "stylelint";
 
@@ -11,7 +12,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Expected $-variable to be first in block"
 });
 
-export default function(primary, options) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primary, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -112,3 +117,7 @@ export default function(primary, options) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-no-missing-interpolation/index.js b/src/rules/dollar-variable-no-missing-interpolation/index.js
index 5338a2e..c041ba3 100644
--- a/src/rules/dollar-variable-no-missing-interpolation/index.js
+++ b/src/rules/dollar-variable-no-missing-interpolation/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 import valueParser from "postcss-value-parser";
 
 export const ruleName = namespace("dollar-variable-no-missing-interpolation");
@@ -9,6 +9,10 @@ export const messages = utils.ruleMessages(ruleName, {
     `Expected variable ${v} to be interpolated when using it with ${n}`
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 // https://developer.mozilla.org/en/docs/Web/CSS/custom-ident#Lists_of_excluded_values
 const customIdentProps = [
   "animation",
@@ -50,7 +54,7 @@ function toRegex(arr) {
   return new RegExp(`(${arr.join("|")})`);
 }
 
-export default function(actual) {
+export default function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
 
@@ -133,3 +137,7 @@ export default function(actual) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-no-namespaced-assignment/index.js b/src/rules/dollar-variable-no-namespaced-assignment/index.js
index 6a54111..8cbc72f 100644
--- a/src/rules/dollar-variable-no-namespaced-assignment/index.js
+++ b/src/rules/dollar-variable-no-namespaced-assignment/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("dollar-variable-no-namespaced-assignment");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected assignment to a namespaced $ variable"
 });
 
-export default function(actual) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
 
@@ -29,3 +33,7 @@ export default function(actual) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/dollar-variable-pattern/index.js b/src/rules/dollar-variable-pattern/index.js
index e03f7a1..d4a09bd 100644
--- a/src/rules/dollar-variable-pattern/index.js
+++ b/src/rules/dollar-variable-pattern/index.js
@@ -1,6 +1,6 @@
 import { isRegExp, isString } from "lodash";
 import { utils } from "stylelint";
-import { namespace, optionsHaveIgnored } from "../../utils";
+import { namespace, optionsHaveIgnored, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("dollar-variable-pattern");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Expected $ variable name to match specified pattern"
 });
 
-export default function(pattern, options) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(pattern, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -61,3 +65,7 @@ export default function(pattern, options) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/double-slash-comment-empty-line-before/index.js b/src/rules/double-slash-comment-empty-line-before/index.js
index 7e46411..ad436a6 100644
--- a/src/rules/double-slash-comment-empty-line-before/index.js
+++ b/src/rules/double-slash-comment-empty-line-before/index.js
@@ -5,7 +5,8 @@ import {
   namespace,
   optionsHaveException,
   optionsHaveIgnored,
-  removeEmptyLinesBefore
+  removeEmptyLinesBefore,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace("double-slash-comment-empty-line-before");
@@ -15,9 +16,13 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected empty line before comment"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const stylelintCommandPrefix = "stylelint-";
 
-export default function(expectation, options, context) {
+export default function rule(expectation, options, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -138,3 +143,7 @@ export default function(expectation, options, context) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/double-slash-comment-inline/index.js b/src/rules/double-slash-comment-inline/index.js
index 6a6ebcb..5bcfdd1 100644
--- a/src/rules/double-slash-comment-inline/index.js
+++ b/src/rules/double-slash-comment-inline/index.js
@@ -2,7 +2,8 @@ import {
   eachRoot,
   findCommentsInRaws,
   namespace,
-  optionsHaveIgnored
+  optionsHaveIgnored,
+  ruleUrl
 } from "../../utils";
 import { utils } from "stylelint";
 
@@ -13,9 +14,13 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected inline //-comment"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const stylelintCommandPrefix = "stylelint-";
 
-export default function(expectation, options) {
+export default function rule(expectation, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -84,3 +89,7 @@ export default function(expectation, options) {
     }
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/double-slash-comment-whitespace-inside/index.js b/src/rules/double-slash-comment-whitespace-inside/index.js
index 43a7911..bf941a1 100644
--- a/src/rules/double-slash-comment-whitespace-inside/index.js
+++ b/src/rules/double-slash-comment-whitespace-inside/index.js
@@ -1,4 +1,4 @@
-import { eachRoot, findCommentsInRaws, namespace } from "../../utils";
+import { eachRoot, findCommentsInRaws, namespace, ruleUrl } from "../../utils";
 import { utils } from "stylelint";
 
 export const ruleName = namespace("double-slash-comment-whitespace-inside");
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected space after //"
 });
 
-export default function(expectation) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation,
@@ -63,3 +67,7 @@ export default function(expectation) {
     }
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/function-color-relative/index.js b/src/rules/function-color-relative/index.js
index fea5e25..04c840e 100644
--- a/src/rules/function-color-relative/index.js
+++ b/src/rules/function-color-relative/index.js
@@ -1,6 +1,6 @@
 import valueParser from "postcss-value-parser";
 import { utils } from "stylelint";
-import { declarationValueIndex, namespace } from "../../utils";
+import { declarationValueIndex, namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("function-color-relative");
 
@@ -8,6 +8,10 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Expected the scale-color function to be used"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const function_names = [
   "saturate",
   "desaturate",
@@ -23,7 +27,7 @@ function isColorFunction(node) {
   return node.type === "function" && function_names.includes(node.value);
 }
 
-function rule(primary) {
+export default function rule(primary) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -67,4 +71,6 @@ function rule(primary) {
   };
 }
 
-export default rule;
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/function-no-unknown/index.js b/src/rules/function-no-unknown/index.js
index 4396721..abbe15f 100644
--- a/src/rules/function-no-unknown/index.js
+++ b/src/rules/function-no-unknown/index.js
@@ -1,7 +1,7 @@
 import { isRegExp, isString } from "lodash";
 import { rules, utils } from "stylelint";
 import valueParser from "postcss-value-parser";
-import { namespace, ALL_FUNCTIONS } from "../../utils";
+import { namespace, ruleUrl, ALL_FUNCTIONS } from "../../utils";
 
 const ruleToCheckAgainst = "function-no-unknown";
 
@@ -15,6 +15,10 @@ export const messages = utils.ruleMessages(ruleName, {
   }
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 export default function rule(primaryOption, secondaryOptions) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
@@ -75,3 +79,7 @@ export default function rule(primaryOption, secondaryOptions) {
     );
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/function-quote-no-quoted-strings-inside/index.js b/src/rules/function-quote-no-quoted-strings-inside/index.js
index 4da1615..98ac362 100644
--- a/src/rules/function-quote-no-quoted-strings-inside/index.js
+++ b/src/rules/function-quote-no-quoted-strings-inside/index.js
@@ -3,7 +3,8 @@ import { utils } from "stylelint";
 import {
   declarationValueIndex,
   isNativeCssFunction,
-  namespace
+  namespace,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace("function-quote-no-quoted-strings-inside");
@@ -12,7 +13,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Quote function used with an already-quoted string"
 });
 
-function rule(primary, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primary, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -73,4 +78,6 @@ function rule(primary, _, context) {
   };
 }
 
-export default rule;
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/function-unquote-no-unquoted-strings-inside/index.js b/src/rules/function-unquote-no-unquoted-strings-inside/index.js
index 7b2dcc2..9d3ba7e 100644
--- a/src/rules/function-unquote-no-unquoted-strings-inside/index.js
+++ b/src/rules/function-unquote-no-unquoted-strings-inside/index.js
@@ -3,7 +3,8 @@ import { utils } from "stylelint";
 import {
   declarationValueIndex,
   isNativeCssFunction,
-  namespace
+  namespace,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace(
@@ -14,7 +15,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unquote function used with an already-unquoted string"
 });
 
-function rule(primary, _, context) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(primary, _, context) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary
@@ -78,4 +83,6 @@ function rule(primary, _, context) {
   };
 }
 
-export default rule;
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/map-keys-quotes/index.js b/src/rules/map-keys-quotes/index.js
index 505d3b7..40d2ec0 100644
--- a/src/rules/map-keys-quotes/index.js
+++ b/src/rules/map-keys-quotes/index.js
@@ -1,6 +1,6 @@
 import valueParser from "postcss-value-parser";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("map-keys-quotes");
 
@@ -8,9 +8,13 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Expected keys in map to be quoted."
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const mathOperators = ["+", "/", "-", "*", "%"];
 
-function rule(primary) {
+export default function rule(primary) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: primary,
@@ -55,6 +59,10 @@ function rule(primary) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 // Takes in a list of map nodes and identifies if they are a map.
 // A map is identified by the pattern: [string/word colon(div) anything comma(div) ...]
 function isMap(nodes) {
@@ -86,5 +94,3 @@ function returnMapKeys(array) {
 
   return new_array;
 }
-
-export default rule;
diff --git a/src/rules/media-feature-value-dollar-variable/index.js b/src/rules/media-feature-value-dollar-variable/index.js
index 85f3be9..7aeff6f 100644
--- a/src/rules/media-feature-value-dollar-variable/index.js
+++ b/src/rules/media-feature-value-dollar-variable/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, optionsHaveIgnored } from "../../utils";
+import { namespace, optionsHaveIgnored, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("media-feature-value-dollar-variable");
 
@@ -9,7 +9,11 @@ export const messages = utils.ruleMessages(ruleName, {
     "Expected a dollar-variable (e.g. $var) to be used as a media feature value"
 });
 
-export default function(expectation, options) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -93,3 +97,7 @@ export default function(expectation, options) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/no-dollar-variables/index.js b/src/rules/no-dollar-variables/index.js
index 7f35332..6491994 100644
--- a/src/rules/no-dollar-variables/index.js
+++ b/src/rules/no-dollar-variables/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("no-dollar-variables");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: variable => `Unexpected dollar variable ${variable}`
 });
 
-export default function(value) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value
@@ -31,3 +35,7 @@ export default function(value) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/no-duplicate-dollar-variables/index.js b/src/rules/no-duplicate-dollar-variables/index.js
index 992daaa..0fd0879 100644
--- a/src/rules/no-duplicate-dollar-variables/index.js
+++ b/src/rules/no-duplicate-dollar-variables/index.js
@@ -1,6 +1,6 @@
 import { utils } from "stylelint";
 import { isString, isBoolean } from "lodash";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("no-duplicate-dollar-variables");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: variable => `Unexpected duplicate dollar variable ${variable}`
 });
 
-export default function(value, secondaryOptions) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value, secondaryOptions) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -178,3 +182,7 @@ export default function(value, secondaryOptions) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/no-duplicate-mixins/index.js b/src/rules/no-duplicate-mixins/index.js
index 8ed45d1..c8e1386 100644
--- a/src/rules/no-duplicate-mixins/index.js
+++ b/src/rules/no-duplicate-mixins/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { atRuleBaseName, namespace } from "../../utils";
+import { atRuleBaseName, namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("no-duplicate-mixins");
 
@@ -7,7 +7,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: mixin => `Unexpected duplicate mixin ${mixin}`
 });
 
-export default function(value) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(value) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value
@@ -41,3 +45,7 @@ export default function(value) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/no-global-function-names/index.js b/src/rules/no-global-function-names/index.js
index 41246d6..11d1dea 100644
--- a/src/rules/no-global-function-names/index.js
+++ b/src/rules/no-global-function-names/index.js
@@ -1,6 +1,6 @@
 import valueParser from "postcss-value-parser";
 import { utils } from "stylelint";
-import { declarationValueIndex, namespace } from "../../utils";
+import { declarationValueIndex, namespace, ruleUrl } from "../../utils";
 
 const interpolationPrefix = /^#{\s*/m;
 
@@ -137,6 +137,10 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: name => errorMessage(name)
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 function errorMessage(name) {
   const sass_package = rules[name];
   const rename = new_rule_names[name];
@@ -155,7 +159,7 @@ function errorMessage(name) {
   }
 }
 
-export default function(value) {
+export default function rule(value) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: value
@@ -187,3 +191,7 @@ export default function(value) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/operator-no-newline-after/index.js b/src/rules/operator-no-newline-after/index.js
index 28bcf46..b885e01 100644
--- a/src/rules/operator-no-newline-after/index.js
+++ b/src/rules/operator-no-newline-after/index.js
@@ -1,5 +1,6 @@
 import { utils } from "stylelint";
-import { eachRoot, namespace, isWhitespace } from "../../utils";
+import { eachRoot, namespace, isWhitespace, ruleUrl } from "../../utils";
+import { calculationOperatorSpaceChecker } from "../operator-no-unspaced";
 
 export const ruleName = namespace("operator-no-newline-after");
 
@@ -7,7 +8,9 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: operator => `Unexpected newline after "${operator}"`
 });
 
-import { calculationOperatorSpaceChecker } from "../operator-no-unspaced";
+export const meta = {
+  url: ruleUrl(ruleName)
+};
 
 /**
  * The checker function: whether there is a newline before THAT operator.
@@ -45,7 +48,7 @@ function checkNewlineBefore({
   }
 }
 
-export default function(expectation) {
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation
@@ -66,3 +69,7 @@ export default function(expectation) {
     }
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/operator-no-newline-before/index.js b/src/rules/operator-no-newline-before/index.js
index 15cc822..0c0fc81 100644
--- a/src/rules/operator-no-newline-before/index.js
+++ b/src/rules/operator-no-newline-before/index.js
@@ -1,5 +1,7 @@
 import { utils } from "stylelint";
-import { eachRoot, namespace, isWhitespace } from "../../utils";
+import { eachRoot, namespace, isWhitespace, ruleUrl } from "../../utils";
+
+import { calculationOperatorSpaceChecker } from "../operator-no-unspaced";
 
 export const ruleName = namespace("operator-no-newline-before");
 
@@ -7,7 +9,9 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: operator => `Unexpected newline before "${operator}"`
 });
 
-import { calculationOperatorSpaceChecker } from "../operator-no-unspaced";
+export const meta = {
+  url: ruleUrl(ruleName)
+};
 
 /**
  * The checker function: whether there is a newline before THAT operator.
@@ -45,7 +49,7 @@ function checkNewlineBefore({
   }
 }
 
-export default function(expectation) {
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation
@@ -66,3 +70,7 @@ export default function(expectation) {
     }
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/operator-no-unspaced/index.js b/src/rules/operator-no-unspaced/index.js
index b33e6fe..c5e5fab 100644
--- a/src/rules/operator-no-unspaced/index.js
+++ b/src/rules/operator-no-unspaced/index.js
@@ -7,7 +7,8 @@ import {
   findCommentsInRaws,
   findOperators,
   isWhitespace,
-  namespace
+  namespace,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace("operator-no-unspaced");
@@ -17,6 +18,10 @@ export const messages = utils.ruleMessages(ruleName, {
   expectedBefore: operator => `Expected single space before "${operator}"`
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 /**
  * The actual check for are there (un)necessary whitespaces
  */
@@ -72,7 +77,7 @@ function newlineBefore(str, startIndex) {
   return false;
 }
 
-export default function(expectation) {
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation
@@ -100,6 +105,10 @@ export default function(expectation) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 /**
  * The core rule logic function. This one can be imported by some other rules
  * that work with Sass operators
diff --git a/src/rules/partial-no-import/index.js b/src/rules/partial-no-import/index.js
index f065701..08b2ab0 100644
--- a/src/rules/partial-no-import/index.js
+++ b/src/rules/partial-no-import/index.js
@@ -1,6 +1,6 @@
 import nodeJsPath from "path";
 import { utils } from "stylelint";
-import { namespace } from "../../utils";
+import { namespace, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("partial-no-import");
 
@@ -8,7 +8,11 @@ export const messages = utils.ruleMessages(ruleName, {
   expected: "Unexpected @import in a partial"
 });
 
-export default function(on) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(on) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: on
@@ -74,3 +78,7 @@ export default function(on) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/percent-placeholder-pattern/index.js b/src/rules/percent-placeholder-pattern/index.js
index 3a15d92..53cada0 100644
--- a/src/rules/percent-placeholder-pattern/index.js
+++ b/src/rules/percent-placeholder-pattern/index.js
@@ -6,7 +6,8 @@ import {
   isStandardRule,
   isStandardSelector,
   parseSelector,
-  namespace
+  namespace,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace("percent-placeholder-pattern");
@@ -16,7 +17,11 @@ export const messages = utils.ruleMessages(ruleName, {
     `Expected %-placeholder "%${placeholder}" to match specified pattern`
 });
 
-export default function(pattern) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(pattern) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: pattern,
@@ -88,3 +93,7 @@ export default function(pattern) {
     }
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/selector-nest-combinators/index.js b/src/rules/selector-nest-combinators/index.js
index 04d5445..4520758 100644
--- a/src/rules/selector-nest-combinators/index.js
+++ b/src/rules/selector-nest-combinators/index.js
@@ -1,5 +1,5 @@
 import { utils } from "stylelint";
-import { namespace, parseSelector } from "../../utils";
+import { namespace, parseSelector, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("selector-nest-combinators");
 
@@ -10,7 +10,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: `Unexpected nesting found in selector`
 });
 
-export default function(expectation) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(expectation) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, {
       actual: expectation,
@@ -149,3 +153,7 @@ export default function(expectation) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/selector-no-redundant-nesting-selector/index.js b/src/rules/selector-no-redundant-nesting-selector/index.js
index 769d5cd..d368eec 100644
--- a/src/rules/selector-no-redundant-nesting-selector/index.js
+++ b/src/rules/selector-no-redundant-nesting-selector/index.js
@@ -5,7 +5,8 @@ import {
   namespace,
   parseSelector,
   hasNestedSibling,
-  isType
+  isType,
+  ruleUrl
 } from "../../utils";
 
 export const ruleName = namespace("selector-no-redundant-nesting-selector");
@@ -14,7 +15,11 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unnecessary nesting selector (&)"
 });
 
-export default function(actual, options) {
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
+export default function rule(actual, options) {
   return (root, result) => {
     const validOptions = utils.validateOptions(
       result,
@@ -79,3 +84,7 @@ export default function(actual, options) {
     });
   };
 }
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
diff --git a/src/rules/selector-no-union-class-name/index.js b/src/rules/selector-no-union-class-name/index.js
index 82e7af1..dfffc7d 100644
--- a/src/rules/selector-no-union-class-name/index.js
+++ b/src/rules/selector-no-union-class-name/index.js
@@ -7,7 +7,7 @@ import {
   isPseudoElement
 } from "postcss-selector-parser";
 import { utils } from "stylelint";
-import { namespace, parseSelector } from "../../utils";
+import { namespace, parseSelector, ruleUrl } from "../../utils";
 
 export const ruleName = namespace("selector-no-union-class-name");
 
@@ -15,6 +15,10 @@ export const messages = utils.ruleMessages(ruleName, {
   rejected: "Unexpected union class name with the parent selector (&)"
 });
 
+export const meta = {
+  url: ruleUrl(ruleName)
+};
+
 const validNestingTypes = [
   isClassName,
   isCombinator,
@@ -24,7 +28,7 @@ const validNestingTypes = [
   isPseudoElement
 ];
 
-export default function(actual) {
+export default function rule(actual) {
   return (root, result) => {
     const validOptions = utils.validateOptions(result, ruleName, { actual });
 
@@ -70,6 +74,10 @@ export default function(actual) {
   };
 }
 
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
 /**
  * Searches for the closest rule which
  * has a selector and returns the selector
diff --git a/src/utils/index.js b/src/utils/index.js
index b46351b..cab6299 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -26,6 +26,7 @@ export { default as parseNestedPropRoot } from "./parseNestedPropRoot";
 export { default as parseSelector } from "./parseSelector";
 export { default as rawNodeString } from "./rawNodeString";
 export { removeEmptyLinesBefore } from "./removeEmptyLinesBefore";
+export * from "./ruleUrl";
 export { default as findOperators } from "./sassValueParser";
 export { default as whitespaceChecker } from "./whitespaceChecker";
 export { default as hasNestedSibling } from "./hasNestedSibling";
diff --git a/src/utils/ruleUrl.js b/src/utils/ruleUrl.js
new file mode 100644
index 0000000..8408983
--- /dev/null
+++ b/src/utils/ruleUrl.js
@@ -0,0 +1,15 @@
+const revision = "master";
+
+/**
+ * Get the specified rule's URL.
+ *
+ * @param {string} ruleName
+ * @return {string}
+ */
+export function ruleUrl(ruleName) {
+  let name = ruleName;
+  if (name.includes("/")) {
+    [, name] = name.split("/", 2);
+  }
+  return `https://github.com/stylelint-scss/stylelint-scss/blob/${revision}/src/rules/${name}`;
+}


Test patch:
diff --git a/src/rules/__tests__/index.test.js b/src/rules/__tests__/index.test.js
new file mode 100644
index 0000000..9589cd3
--- /dev/null
+++ b/src/rules/__tests__/index.test.js
@@ -0,0 +1,26 @@
+import rules from "../index";
+
+const ruleEntries = Object.entries(rules);
+
+test("not empty", () => {
+  expect(ruleEntries.length).toBeGreaterThan(0);
+});
+
+for (const [ruleName, rule] of ruleEntries) {
+  test(`"${ruleName}" is a function`, () => {
+    expect(rule).toBeInstanceOf(Function);
+  });
+
+  test(`"${ruleName}" has the "ruleName" property`, () => {
+    expect(rule).toHaveProperty("ruleName", expect.stringMatching(ruleName));
+  });
+
+  test(`"${ruleName}" has the "messages" property`, () => {
+    expect(rule).toHaveProperty("messages", expect.any(Object));
+  });
+
+  test(`"${ruleName}" has the "meta" property`, () => {
+    expect(rule).toHaveProperty("meta", expect.any(Object));
+    expect(rule).toHaveProperty("meta.url", expect.stringMatching(ruleName));
+  });
+}
diff --git a/src/utils/__tests__/ruleUrl.test.js b/src/utils/__tests__/ruleUrl.test.js
new file mode 100644
index 0000000..619b0fa
--- /dev/null
+++ b/src/utils/__tests__/ruleUrl.test.js
@@ -0,0 +1,13 @@
+import { ruleUrl } from "../ruleUrl";
+
+test("returning a rule URL", () => {
+  expect(ruleUrl("at-function-pattern")).toEqual(
+    "https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/at-function-pattern"
+  );
+});
+
+test("returning a rule URL removing the namespace", () => {
+  expect(ruleUrl("scss/at-function-pattern")).toEqual(
+    "https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/at-function-pattern"
+  );
+});