From 5ae7ec9fade6b4eb9cbebf92e0c06176436225ef Mon Sep 17 00:00:00 2001 From: krister Date: Mon, 26 Aug 2019 23:56:40 +0300 Subject: [PATCH] Add at-import-partial-extension rule --- .../at-import-partial-extension/README.md | 116 ++++++ .../__tests__/index.js | 332 ++++++++++++++++++ .../at-import-partial-extension/index.js | 86 +++++ src/rules/index.js | 10 +- 4 files changed, 540 insertions(+), 4 deletions(-) create mode 100644 src/rules/at-import-partial-extension/README.md create mode 100644 src/rules/at-import-partial-extension/__tests__/index.js create mode 100644 src/rules/at-import-partial-extension/index.js diff --git a/src/rules/at-import-partial-extension/README.md b/src/rules/at-import-partial-extension/README.md new file mode 100644 index 00000000..a93c70aa --- /dev/null +++ b/src/rules/at-import-partial-extension/README.md @@ -0,0 +1,116 @@ +# at-import-partial-extension + +Require or disallow extension in `@import` commands. + +```scss +@import "file.scss"; +/** ↑ + * This extension */ +``` + +The rule ignores [cases](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#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. + +## Options + +`string`: `"always"|"never"` + +### `"always"` + +The following patterns are considered warnings: + +```scss +@import "foo"; +``` + +```scss +@import "path/fff"; +``` + +```scss +@import "path\\fff"; +``` + +```scss +@import "df/fff", "1.SCSS"; +``` + +The following patterns are _not_ considered warnings: + +```scss +@import "fff.scss"; +``` + +```scss +@import "path/fff.scss"; +``` + +```scss +@import url("path/_file.css"); /* has url(), so doesn't count as a partial @import */ +``` + +```scss +@import "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 */ +``` + +### `"never"` + +The following patterns are considered warnings: + +```scss +@import "foo.scss"; +``` + +```scss +@import "path/fff.less"; +``` + +```scss +@import "path\\fff.ruthless"; +``` + +```scss +@import "df/fff", "1.SCSS"; +``` + +The following patterns are _not_ considered warnings: + +```scss +@import "foo"; +``` + +```scss +@import "path/fff"; +``` + +```scss +@import url("path/_file.css"); /* has url(), so doesn't count as a partial @import */ +``` + +```scss +@import "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 */ +``` diff --git a/src/rules/at-import-partial-extension/__tests__/index.js b/src/rules/at-import-partial-extension/__tests__/index.js new file mode 100644 index 00000000..c57e452d --- /dev/null +++ b/src/rules/at-import-partial-extension/__tests__/index.js @@ -0,0 +1,332 @@ +import rule, { messages, ruleName } from ".."; + +testRule(rule, { + ruleName, + config: ["always"], + syntax: "scss", + + accept: [ + { + code: ` + @import "fff.scss"; + `, + description: "Single file, .scss extension" + }, + { + code: ` + @import "path/fff.scss"; + `, + description: "Single file, path with dir, .scss extension" + }, + { + code: ` + @import "df\\fff.scss"; + `, + description: + "Single file, path with dir, has extension, windows delimiters." + }, + { + code: ` + @import 'fff.scss'; + `, + description: "Single file, .scss extension, single quotes." + }, + { + code: ` + @import "fff.foo"; + `, + description: "Single file, .foo extension." + }, + { + code: ` + @import " fff.scss1 "; + `, + description: "Single file, extension, trailing whitespaces." + }, + { + code: ` + @import "fff.scss", "fff.moi"; + `, + description: "Multiple files with extensions." + }, + { + code: ` + @import url("path/_file.css"); + `, + description: "Import CSS with url()." + }, + { + code: ` + @import "_file.css"; + `, + description: "Import CSS by extension." + }, + { + 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: ` + @import "screen.scss"; + `, + description: "Import with a name that matches a media query type." + } + ], + + reject: [ + { + code: ` + @import "fff"; + `, + line: 2, + column: 7, + message: messages.expected, + description: "Single file, no extension." + }, + { + code: ` + @import "fff", + "fff.ruthless"; + `, + line: 2, + column: 7, + message: messages.expected, + description: "Multiple files, one without extension." + }, + { + code: ` + @import "fff", "score"; + `, + line: 2, + column: 7, + message: messages.expected, + description: "Two files, no extensions." + } + ] +}); + +testRule(rule, { + ruleName, + config: ["never"], + syntax: "scss", + + accept: [ + { + code: ` + @import "fff"; + `, + description: "Single file, no extension, double quotes." + }, + { + code: ` + @import 'fff'; + `, + description: "Single file, no extension, single quotes." + }, + { + code: ` + @import ' fff '; + `, + description: "Single file, no extension, trailing spaces inside quotes." + }, + { + code: ` + @import "fff", "score"; + `, + description: "Two files, no extension, double quotes." + }, + { + code: ` + @import "screen"; + `, + description: "Import with a name that matches a media query type." + }, + { + code: ` + @import url("path/_file.css"); + `, + description: "Import CSS with url()." + }, + { + code: ` + @import "_file.css"; + `, + description: "Import CSS by extension." + }, + { + 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)." + } + ], + + reject: [ + { + code: ` + @import "fff.scss"; + `, + line: 2, + column: 20, + message: messages.rejected("scss"), + description: "Single file, .scss extension." + }, + { + code: ` + @import "screen.scss"; + `, + line: 2, + column: 23, + message: messages.rejected("scss"), + description: "Single file with media query type as name, .scss extension." + }, + { + code: ` + @import "fff.scss "; + `, + line: 2, + column: 20, + message: messages.rejected("scss"), + description: "Single file, has extension, space at the end." + }, + { + code: ` + @import " fff.scss "; + `, + line: 2, + column: 21, + message: messages.rejected("scss"), + description: "Single file, has extension, trailing spaces." + }, + { + code: ` + @import "df/fff.scss"; + `, + line: 2, + column: 23, + message: messages.rejected("scss"), + description: "Single file, path with dir, has extension." + }, + { + code: ` + @import "df\\fff.scss"; + `, + line: 2, + column: 23, + message: messages.rejected("scss"), + description: + "Single file, path with dir, has extension, windows delimiters." + }, + { + code: ` + @import "df/fff", '_1.scss'; + `, + line: 2, + column: 29, + message: messages.rejected("scss"), + description: "Two files, path with dir, has extension." + } + ] +}); diff --git a/src/rules/at-import-partial-extension/index.js b/src/rules/at-import-partial-extension/index.js new file mode 100644 index 00000000..423bbfd2 --- /dev/null +++ b/src/rules/at-import-partial-extension/index.js @@ -0,0 +1,86 @@ +import nodeJsPath from "path"; +import { utils } from "stylelint"; +import { namespace } from "../../utils"; + +export const ruleName = namespace("at-import-partial-extension"); + +export const messages = utils.ruleMessages(ruleName, { + expected: "Expected @import to have an extension", + rejected: ext => `Unexpected extension ".${ext}" in @import` +}); + +// https://drafts.csswg.org/mediaqueries/#media-types +const mediaQueryTypes = [ + "all", + "print", + "screen", + "speech", + "tv", + "tty", + "projection", + "handheld", + "braille", + "embossed", + "aural" +]; + +const mediaQueryTypesRE = new RegExp(`(${mediaQueryTypes.join("|")})$`, "i"); +const stripPath = path => + path.replace(/^\s*?("|')\s*/, "").replace(/\s*("|')\s*?$/, ""); + +export default function(expectation) { + return (root, result) => { + const validOptions = utils.validateOptions(result, ruleName, { + actual: expectation, + possible: ["always", "never"] + }); + + if (!validOptions) { + return; + } + + root.walkAtRules("import", decl => { + const paths = decl.params + .split(",") + .filter(path => !mediaQueryTypesRE.test(path.trim())); + + // Processing comma-separated lists of import paths + paths.forEach(path => { + // Stripping trailing quotes and whitespaces, if any + const pathStripped = stripPath(path); + + // Skipping importing CSS: url(), ".css", URI with a protocol + if ( + pathStripped.slice(0, 4) === "url(" || + pathStripped.slice(-4) === ".css" || + pathStripped.search("//") !== -1 + ) { + return; + } + + const extension = nodeJsPath.extname(pathStripped).slice(1); + + if (!extension && expectation === "always") { + utils.report({ + message: messages.expected, + node: decl, + result, + ruleName + }); + + return; + } + + if (extension && expectation === "never") { + utils.report({ + message: messages.rejected(extension), + node: decl, + word: extension, + result, + ruleName + }); + } + }); + }); + }; +} diff --git a/src/rules/index.js b/src/rules/index.js index e90fce82..a24dc219 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -1,8 +1,9 @@ -import atExtendNoMissingPlaceholder from "./at-extend-no-missing-placeholder"; +import atEachKeyValue from "./at-each-key-value-single-line"; import atElseClosingBraceNewlineAfter from "./at-else-closing-brace-newline-after"; import atElseClosingBraceSpaceAfter from "./at-else-closing-brace-space-after"; import atElseEmptyLineBefore from "./at-else-empty-line-before"; import atElseIfParenthesesSpaceBefore from "./at-else-if-parentheses-space-before"; +import atExtendNoMissingPlaceholder from "./at-extend-no-missing-placeholder"; import atFunctionNamedArguments from "./at-function-named-arguments"; import atFunctionParenthesesSpaceBefore from "./at-function-parentheses-space-before"; import atFunctionPattern from "./at-function-pattern"; @@ -10,13 +11,13 @@ import atIfClosingBraceNewlineAfter from "./at-if-closing-brace-newline-after"; import atIfClosingBraceSpaceAfter from "./at-if-closing-brace-space-after"; import atIfNoNull from "./at-if-no-null"; import atImportNoPartialLeadingUnderscore from "./at-import-no-partial-leading-underscore"; +import atImportPartialExtension from "./at-import-partial-extension"; import atImportPartialExtensionBlacklist from "./at-import-partial-extension-blacklist"; import atImportPartialExtensionWhitelist from "./at-import-partial-extension-whitelist"; import atMixinArgumentlessCallParentheses from "./at-mixin-argumentless-call-parentheses"; import atMixinNamedArguments from "./at-mixin-named-arguments"; import atMixinParenthesesSpaceBefore from "./at-mixin-parentheses-space-before"; import atMixinPattern from "./at-mixin-pattern"; -import atEachKeyValue from "./at-each-key-value-single-line"; import atRuleConditionalNoParen from "./at-rule-conditional-no-parentheses"; import atRuleNoUnknown from "./at-rule-no-unknown"; import commentNoLoud from "./comment-no-loud"; @@ -33,12 +34,12 @@ import dollarVariablePattern from "./dollar-variable-pattern"; import doubleSlashCommentEmptyLineBefore from "./double-slash-comment-empty-line-before"; import doubleSlashCommentInline from "./double-slash-comment-inline"; import doubleSlashCommentWhitespaceInside from "./double-slash-comment-whitespace-inside"; +import functionColorRelative from "./function-color-relative"; import functionNoQuotedStrings from "./function-quote-no-quoted-strings-inside"; import functionNoUnquotedStrings from "./function-unquote-no-unquoted-strings-inside"; -import functionColorRelative from "./function-color-relative"; +import mapKeysQuotes from "./map-keys-quotes"; import mediaFeatureValueDollarVariable from "./media-feature-value-dollar-variable"; import noDollarVariables from "./no-dollar-variables"; -import mapKeysQuotes from "./map-keys-quotes"; import noDuplicateDollarVariables from "./no-duplicate-dollar-variables"; import operatorNoNewlineAfter from "./operator-no-newline-after"; import operatorNoNewlineBefore from "./operator-no-newline-before"; @@ -62,6 +63,7 @@ export default { "at-if-closing-brace-space-after": atIfClosingBraceSpaceAfter, "at-if-no-null": atIfNoNull, "at-import-no-partial-leading-underscore": atImportNoPartialLeadingUnderscore, + "at-import-partial-extension": atImportPartialExtension, "at-import-partial-extension-blacklist": atImportPartialExtensionBlacklist, "at-import-partial-extension-whitelist": atImportPartialExtensionWhitelist, "at-mixin-argumentless-call-parentheses": atMixinArgumentlessCallParentheses,