Repository: stylelint-scss/stylelint-scss (js)

Issue:
Deprecate `at-import-no-partial-leading-underscore` and add `load-no-partial-leading-underscore`.
The same rules in [at-import-no-partial-leading-underscore](https://g3doc.corp.google.com/third_party/javascript/node_modules/stylelint_scss/v5_2_1/src/rules/at-import-no-partial-leading-underscore/README.md?cl=head) of disallowing leading underscores and file extensions apply to [@use](https://sass-lang.com/documentation/at-rules/use/), [@forward](https://sass-lang.com/documentation/at-rules/forward/) and [meta.load-css()](https://sass-lang.com/documentation/modules/meta/#mixins) url parameter. The existing `at-import-no-partial-leading-underscore` rule could be expanded to cover all these cases and then be renamed to `load-no-partial-leading-underscore`.

Pull request:
Add `load-no-partial-leading-underscore` rule.
Fix #859.

Patch:
diff --git a/README.md b/README.md
index 407a9b4..644b414 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,6 @@ Please also see the [example configs](./docs/examples/) for special cases.
 
 ### `@`-import
 
-- [`at-import-no-partial-leading-underscore`](./src/rules/at-import-no-partial-leading-underscore/README.md): Disallow leading underscore in partial names in `@import`.
 - [`at-import-partial-extension`](./src/rules/at-import-partial-extension/README.md): Require or disallow extension in `@import` commands (Autofixable).
 - [`at-import-partial-extension-blacklist`](./src/rules/at-import-partial-extension-blacklist/README.md): Specify a blacklist of disallowed file extensions for partial names in `@import` commands.
 - [`at-import-partial-extension-whitelist`](./src/rules/at-import-partial-extension-whitelist/README.md): Specify a whitelist of allowed file extensions for partial names in `@import` commands.
@@ -180,6 +179,10 @@ Please also see the [example configs](./docs/examples/) for special cases.
 - [`selector-no-redundant-nesting-selector`](./src/rules/selector-no-redundant-nesting-selector/README.md): Disallow redundant nesting selectors (`&`).
 - [`selector-no-union-class-name`](./src/rules/selector-no-union-class-name/README.md): Disallow union class names with the parent selector (`&`).
 
+### Load
+
+- [`load-no-partial-leading-underscore`](./src/rules/load-no-partial-leading-underscore/README.md): Disallow leading underscore in partial names in `@import`, `@use`, `@forward`, and [`meta.load-css`](https://sass-lang.com/documentation/modules/meta/#load-css) `$url` parameter.
+
 ### General / Sheet
 
 - [`no-dollar-variables`](./src/rules/no-dollar-variables/README.md): Disallow dollar variables within a stylesheet.
@@ -187,6 +190,11 @@ Please also see the [example configs](./docs/examples/) for special cases.
 - [`no-duplicate-mixins`](./src/rules/no-duplicate-mixins/README.md): Disallow duplicate mixins within a stylesheet.
 - [`no-global-function-names`](./src/rules/no-global-function-names/README.md): Disallows the use of global function names, as these global functions are now located inside built-in Sass modules.
 
+## Deprecated
+These rules are deprecated — we won't fix bugs nor add options, and we will remove them in the next major release. We recommend you use a pretty printer (like Prettier) alongside Stylelint rather than these rules.
+
+- [`at-import-no-partial-leading-underscore`](https://github.com/stylelint-scss/stylelint-scss/blob/v5.2.1/src/rules/at-import-no-partial-leading-underscore/README.md): Disallow leading underscore in partial names in `@import`.
+
 ## Help out
 
 The work on the plugin's rules is still in progress, so if you feel like it, you're welcome to help out with any of these (the plugin follows Stylelint guidelines so most of this is based on its docs):
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 349281c..36305bd 100644
--- a/src/rules/at-import-no-partial-leading-underscore/index.js
+++ b/src/rules/at-import-no-partial-leading-underscore/index.js
@@ -11,7 +11,8 @@ const messages = utils.ruleMessages(ruleName, {
 });
 
 const meta = {
-  url: ruleUrl(ruleName)
+  url: ruleUrl(ruleName),
+  deprecated: true
 };
 
 function rule(actual) {
@@ -22,6 +23,16 @@ function rule(actual) {
       return;
     }
 
+    result.warn(
+      "'at-import-no-partial-leading-underscore' has been deprecated, " +
+        "and will be removed in '6.0'. Use 'load-no-partial-leading-underscore' instead.",
+      {
+        stylelintType: "deprecation",
+        stylelintReference:
+          "https://github.com/stylelint-scss/stylelint-scss/blob/v5.2.1/src/rules/at-import-no-partial-leading-underscore/README.md"
+      }
+    );
+
     function checkPathForUnderscore(path, decl) {
       // Stripping trailing quotes and whitespaces, if any
       const pathStripped = path
diff --git a/src/rules/index.js b/src/rules/index.js
index 6286ea9..6873e31 100644
--- a/src/rules/index.js
+++ b/src/rules/index.js
@@ -14,6 +14,7 @@ const rules = {
   "at-if-closing-brace-space-after": require("./at-if-closing-brace-space-after"),
   "at-if-no-null": require("./at-if-no-null"),
   "at-import-no-partial-leading-underscore": require("./at-import-no-partial-leading-underscore"),
+  "load-no-partial-leading-underscore": require("./load-no-partial-leading-underscore"),
   "at-import-partial-extension-blacklist": require("./at-import-partial-extension-blacklist"),
   "at-import-partial-extension-whitelist": require("./at-import-partial-extension-whitelist"),
   "at-import-partial-extension": require("./at-import-partial-extension"),
diff --git a/src/rules/load-no-partial-leading-underscore/README.md b/src/rules/load-no-partial-leading-underscore/README.md
new file mode 100644
index 0000000..2645a84
--- /dev/null
+++ b/src/rules/load-no-partial-leading-underscore/README.md
@@ -0,0 +1,73 @@
+# load-no-partial-leading-underscore
+
+Disallow leading underscore in partial names in `@import`, `@use`, `@forward`, and [`meta.load-css`](https://sass-lang.com/documentation/modules/meta/#load-css) `$url` parameter.
+
+```scss
+@import "path/to/_file";
+/**              ↑
+ *   Disallow this */
+```
+
+The rule ignores [cases](https://sass-lang.com/documentation/at-rules/import) when Sass considers an `@import` command just a plain CSS import:
+
+* If the file’s extension is `.css`.
+* If the filename begins with `http://` (or any other protocol).
+* If the filename is a `url()`.
+* If the `@import` has any media queries.
+
+
+The following patterns are considered warnings:
+
+```scss
+@import "_foo";
+```
+
+```scss
+@import "path/_fff";
+```
+
+```scss
+@forward "path\\_fff"; /* Windows delimiters */
+```
+
+```scss
+@use "df/fff", "_1.scss";
+```
+
+```scss
+@use "sass:meta";
+.a {
+  @include meta.load-css("_fff", $with: ("border-contrast": true));
+}
+```
+
+The following patterns are _not_ considered warnings:
+
+```scss
+@use "_path/fff"; /* underscore in a directory name, not in a partial name */
+```
+
+```scss
+@import url("path/_file.css"); /* has url(), so doesn't count as a partial @import */
+```
+
+```scss
+@forward "_file.css"; /* Has ".css" extension, so doesn't count as a partial @import */
+```
+
+```scss
+/* Both are URIs, so don't count as partial @imports */
+@import "http://_file.scss";
+@import "//_file.scss";
+```
+
+```scss
+@import "_file.scss" screen; /* Has a media query, so doesn't count as a partial @import */
+```
+
+```scss
+@use "sass:meta";
+.a {
+  @include meta.load-css("dark-theme/code", $with: ("border-contrast": true));
+}
+```
diff --git a/src/rules/load-no-partial-leading-underscore/index.js b/src/rules/load-no-partial-leading-underscore/index.js
new file mode 100644
index 0000000..4272982
--- /dev/null
+++ b/src/rules/load-no-partial-leading-underscore/index.js
@@ -0,0 +1,84 @@
+"use strict";
+
+const { utils } = require("stylelint");
+const namespace = require("../../utils/namespace");
+const ruleUrl = require("../../utils/ruleUrl");
+
+const ruleName = namespace("load-no-partial-leading-underscore");
+
+const messages = utils.ruleMessages(ruleName, {
+  expected: "Unexpected leading underscore in imported partial name"
+});
+
+const meta = {
+  url: ruleUrl(ruleName)
+};
+
+function rule(actual) {
+  return (root, result) => {
+    const validOptions = utils.validateOptions(result, ruleName, { actual });
+    const hasArgumentsRegExp = /\(\s*([^)]+?)\s*\)/;
+
+    if (!validOptions) {
+      return;
+    }
+
+    function checkPathForUnderscore(path, decl) {
+      // Stripping trailing quotes and whitespaces, if any
+      const pathStripped = path
+        .replace(/^\s*(["'])\s*/, "")
+        .replace(/\s*(["'])\s*$/, "");
+
+      // Searching a _ at the start of filename
+      if (pathStripped.search(/(?:^|\/|\\)_[^/]+$/) === -1) {
+        return;
+      }
+
+      // Skipping importing CSS: url(), ".css", URI with a protocol, media
+      if (
+        pathStripped.slice(0, 4) === "url(" ||
+        pathStripped.slice(-4) === ".css" ||
+        pathStripped.search("//") !== -1 ||
+        pathStripped.search(/[\s,)"']\w+$/) !== -1
+      ) {
+        return;
+      }
+
+      utils.report({
+        message: messages.expected,
+        node: decl,
+        result,
+        ruleName
+      });
+    }
+
+    root.walkAtRules(atrule => {
+      if (
+        atrule.name === "import" ||
+        atrule.name === "use" ||
+        atrule.name === "forward"
+      ) {
+        // Processing comma-separated lists of import paths
+        atrule.params.split(",").forEach(path => {
+          checkPathForUnderscore(path, atrule);
+        });
+      }
+      if (atrule.name === "include") {
+        // Processing meta.load-css url
+        if (atrule.params.match(/load-css/)) {
+          const args = hasArgumentsRegExp.exec(atrule.params);
+          if (args) {
+            const arg = args[0].split(",");
+            checkPathForUnderscore(arg[0].replace(/\(|\)/, ""), atrule);
+          }
+        }
+      }
+    });
+  };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+rule.meta = meta;
+
+module.exports = rule;


Test patch:
diff --git a/src/rules/load-no-partial-leading-underscore/__tests__/index.js b/src/rules/load-no-partial-leading-underscore/__tests__/index.js
new file mode 100644
index 0000000..a50042a
--- /dev/null
+++ b/src/rules/load-no-partial-leading-underscore/__tests__/index.js
@@ -0,0 +1,328 @@
+"use strict";
+
+const { messages, ruleName } = require("..");
+
+testRule({
+  ruleName,
+  config: [true],
+  customSyntax: "postcss-scss",
+
+  accept: [
+    {
+      code: `
+      @import "fff";
+    `,
+      description: "Single file, no underscore, double quotes."
+    },
+    {
+      code: `
+      @import 'fff';
+    `,
+      description: "Single file, no underscore, single quotes."
+    },
+    {
+      code: `
+      @import ' fff ';
+    `,
+      description: "Single file, no underscore, trailing spaces inside quotes."
+    },
+    {
+      code: `
+      @import "fff", "score";
+    `,
+      description: "Two files, no underscore, double quotes."
+    },
+    {
+      code: `
+      @import "ff_f";
+    `,
+      description: "One file, underscore in the middle."
+    },
+    {
+      code: `
+        @import "fff_",
+        "score";
+    `,
+      description: "One file, underscore at the end."
+    },
+    {
+      code: `
+      @import "_path/fff";
+    `,
+      description:
+        "One file, path with a dir, underscore in dir name, double quotes."
+    },
+    {
+      code: `
+      @import url("path/_file.css");
+    `,
+      description: "Import CSS with url()."
+    },
+    {
+      code: `
+      @import "_file.css";
+    `,
+      description: "Import CSS by extension."
+    },
+    {
+      code: `
+      @import "_file.css ";
+    `,
+      description: "Import CSS by extension, trailing spaces."
+    },
+    {
+      code: `
+      @import "http://_file.scss";
+    `,
+      description: "Import CSS from the web, http://."
+    },
+    {
+      code: `
+      @import " https://_file.scss ";
+    `,
+      description:
+        "Import CSS from the web, https://, trailing spaces inside quotes"
+    },
+    {
+      code: `
+      @import "//_file.scss";
+    `,
+      description: "Import CSS from the web, no protocol."
+    },
+    {
+      code: `
+      @import "_file.scss" screen;
+    `,
+      description: "Import CSS (with media queries)."
+    },
+    {
+      code: `
+      @import "_file.scss"screen;
+    `,
+      description: "Import CSS (with media queries)."
+    },
+    {
+      code: `
+      @import "_file.scss "screen;
+    `,
+      description:
+        "Import CSS (with media queries), trailing space inside quotes."
+    },
+    {
+      code: `
+      @import url(_lol.scss) screen;
+    `,
+      description: "Import CSS (with media queries - url + media)."
+    },
+    {
+      code: `
+      @import _file.scss tv, screen;
+    `,
+      description: "Import CSS (with media queries - multiple)."
+    },
+    {
+      code: `
+      @import _file.scss tv,screen;
+    `,
+      description: "Import CSS (with media queries - multiple, no spaces)."
+    },
+    {
+      code: `
+      @use "sass:meta";
+      .a {
+        @include meta.load-css();
+      }
+    `,
+      description: "meta.load-css, no parameters."
+    }
+  ],
+
+  reject: [
+    {
+      code: `
+      @import "_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@import one file, underscore at the start."
+    },
+    {
+      code: `
+      @import "_fff ";
+    `,
+      line: 2,
+      message: messages.expected,
+      description:
+        "@import one file, underscore at the start, trailing space at the end."
+    },
+    {
+      code: `
+      @import " _fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description:
+        "@import one file, underscore at the start, trailing space at the start."
+    },
+    {
+      code: `
+      @import "df/_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@import one file, path with dir, underscore at the start."
+    },
+    {
+      code: `
+      @import "df\\_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@import one file, path with dir, windows delimiters."
+    },
+    {
+      code: `
+      @import "df/fff", '_1.scss';
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@import two files, path with dir, underscore at the start."
+    },
+    {
+      code: `
+      @use "_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@use one file, underscore at the start."
+    },
+    {
+      code: `
+      @use "_fff ";
+    `,
+      line: 2,
+      message: messages.expected,
+      description:
+        "@use one file, underscore at the start, trailing space at the end."
+    },
+    {
+      code: `
+      @use " _fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description:
+        "@use one file, underscore at the start, trailing space at the start."
+    },
+    {
+      code: `
+      @use "df/_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@use one file, path with dir, underscore at the start."
+    },
+    {
+      code: `
+      @use "df\\_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@use one file, path with dir, windows delimiters."
+    },
+    {
+      code: `
+      @use "df/fff", '_1.scss';
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@use two files, path with dir, underscore at the start."
+    },
+    {
+      code: `
+      @forward "_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@forward one file, underscore at the start."
+    },
+    {
+      code: `
+      @forward "_fff ";
+    `,
+      line: 2,
+      message: messages.expected,
+      description:
+        "@forward one file, underscore at the start, trailing space at the end."
+    },
+    {
+      code: `
+      @forward " _fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description:
+        "@forward one file, underscore at the start, trailing space at the start."
+    },
+    {
+      code: `
+      @forward "df/_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@forward one file, path with dir, underscore at the start."
+    },
+    {
+      code: `
+      @forward "df\\_fff";
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@forward one file, path with dir, windows delimiters."
+    },
+    {
+      code: `
+      @forward "df/fff", '_1.scss';
+    `,
+      line: 2,
+      message: messages.expected,
+      description: "@forward two files, path with dir, underscore at the start."
+    },
+    {
+      code: `
+      @use "sass:meta";
+      .a {
+        @include meta.load-css("df\\_fff", $with: ("border-contrast": true));
+      }
+    `,
+      line: 4,
+      message: messages.expected,
+      description:
+        "meta.load-css two params, path with dir, windows delimiters."
+    },
+    {
+      code: `
+      @use "sass:meta";
+      .a {
+        @include meta.load-css("_fff", $with: ("border-contrast": true));
+      }
+    `,
+      line: 4,
+      message: messages.expected,
+      description:
+        "meta.load-css two params, underscore at the start, trailing space at the start."
+    },
+    {
+      code: `
+      @use "sass:meta";
+      .a {
+        @include meta.load-css("_fff");
+      }
+    `,
+      line: 4,
+      message: messages.expected,
+      description:
+        "meta.load-css one param, underscore at the start, trailing space at the start."
+    }
+  ]
+});