diff --git a/src/utils/__tests__/hasInterpolation.test.js b/src/utils/__tests__/hasInterpolation.test.js new file mode 100644 index 00000000..637d0011 --- /dev/null +++ b/src/utils/__tests__/hasInterpolation.test.js @@ -0,0 +1,17 @@ +import hasInterpolation from "../hasInterpolation"; + +it("hasInterpolation", () => { + expect(hasInterpolation("(min-width#{$value}: 10px)")).toBeTruthy(); + expect(hasInterpolation("(@{value}min-width : 10px)")).toBeTruthy(); + expect(hasInterpolation("#{$Attr}-color")).toBeTruthy(); + expect(hasInterpolation("@{Attr}-color")).toBeTruthy(); + expect(hasInterpolation("#{50% - $n}")).toBeTruthy(); + expect(hasInterpolation(".n-#{$n}")).toBeTruthy(); + expect(hasInterpolation(":n-#{$n}")).toBeTruthy(); + expect(hasInterpolation(".n-@{n}")).toBeTruthy(); + expect(hasInterpolation("(min-width: 10px)")).toBeFalsy(); + expect(hasInterpolation(".a{}")).toBeFalsy(); + expect(hasInterpolation("$sass-variable + 'foo'")).toBeFalsy(); + expect(hasInterpolation("10px")).toBeFalsy(); + expect(hasInterpolation("@less-variable + 'foo'")).toBeFalsy(); +}); diff --git a/src/utils/__tests__/isStandardSyntaxProperty.test.js b/src/utils/__tests__/isStandardSyntaxProperty.test.js new file mode 100644 index 00000000..198ef79e --- /dev/null +++ b/src/utils/__tests__/isStandardSyntaxProperty.test.js @@ -0,0 +1,31 @@ +import isStandardSyntaxProperty from "../isStandardSyntaxProperty"; + +describe("isStandardSyntaxProperty", () => { + it("single word", () => { + expect(isStandardSyntaxProperty("top")).toBeTruthy(); + }); + it("custom property", () => { + expect(isStandardSyntaxProperty("--custom-property")).toBeTruthy(); + }); + it("hyphenated words", () => { + expect(isStandardSyntaxProperty("border-top-left-radius")).toBeTruthy(); + }); + it("vendor prefix", () => { + expect(isStandardSyntaxProperty("-webkit-appearance")).toBeTruthy(); + }); + it("sass variable", () => { + expect(isStandardSyntaxProperty("$sass-variable")).toBeFalsy(); + }); + it("sass interpolation", () => { + expect(isStandardSyntaxProperty("#{$Attr}-color")).toBeFalsy(); + }); + it("less variable", () => { + expect(isStandardSyntaxProperty("@{Attr}-color")).toBeFalsy(); + }); + it("less append property value with comma", () => { + expect(isStandardSyntaxProperty("transform+")).toBeFalsy(); + }); + it("less append property value with space", () => { + expect(isStandardSyntaxProperty("transform+_")).toBeFalsy(); + }); +}); diff --git a/src/utils/__tests__/isStandardSyntaxSelector.test.js b/src/utils/__tests__/isStandardSyntaxSelector.test.js new file mode 100644 index 00000000..674d1e36 --- /dev/null +++ b/src/utils/__tests__/isStandardSyntaxSelector.test.js @@ -0,0 +1,78 @@ +import isStandardSyntaxSelector from "../isStandardSyntaxSelector"; + +describe("isStandardSyntaxSelector", () => { + it("type", () => { + expect(isStandardSyntaxSelector("a")).toBeTruthy(); + }); + it("class", () => { + expect(isStandardSyntaxSelector(".a")).toBeTruthy(); + }); + it("attribute", () => { + expect(isStandardSyntaxSelector("[a=a]")).toBeTruthy(); + }); + it("universal", () => { + expect(isStandardSyntaxSelector("*")).toBeTruthy(); + }); + it("pseudo-class", () => { + expect(isStandardSyntaxSelector("a:last-child")).toBeTruthy(); + }); + it("pseudo-class with function", () => { + expect(isStandardSyntaxSelector("a:not(.b)")).toBeTruthy(); + }); + it("pseudo-element", () => { + expect(isStandardSyntaxSelector("a::after")).toBeTruthy(); + }); + it("compound", () => { + expect(isStandardSyntaxSelector("a.b")).toBeTruthy(); + }); + it("complex", () => { + expect(isStandardSyntaxSelector("a > b")).toBeTruthy(); + }); + it("list", () => { + expect(isStandardSyntaxSelector("a, b")).toBeTruthy(); + }); + it("SCSS interpolation (id)", () => { + expect(isStandardSyntaxSelector("#{50% - $n}")).toBeFalsy(); + }); + it("SCSS interpolation (class)", () => { + expect(isStandardSyntaxSelector(".n-#{$n}")).toBeFalsy(); + }); + it("SCSS interpolation (pseudo)", () => { + expect(isStandardSyntaxSelector(":n-#{$n}")).toBeFalsy(); + }); + it("Less interpolation", () => { + expect(isStandardSyntaxSelector(".n-@{n}")).toBeFalsy(); + }); + it("Less extend", () => { + expect(isStandardSyntaxSelector(".a:extend(.a)")).toBeFalsy(); + }); + it("Less extend `all`", () => { + expect(isStandardSyntaxSelector(".a:extend(.a all)")).toBeFalsy(); + }); + it("Less extend inside ruleset", () => { + expect(isStandardSyntaxSelector("a { &:extend(.a all) }")).toBeFalsy(); + }); + it("SCSS placeholder", () => { + expect(isStandardSyntaxSelector("%foo")).toBeFalsy(); + }); + it("Less mixin with resolved nested selectors", () => { + expect(isStandardSyntaxSelector(".foo().bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo(@a, @b).bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo()#bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo()#bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo() bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo() + bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo() > bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo() ~ bar")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo()[bar]")).toBeFalsy(); + expect(isStandardSyntaxSelector(".foo()[bar='baz']")).toBeFalsy(); + }); + + it("ERB templates", () => { + // E. g. like in https://github.com/stylelint/stylelint/issues/4489 + expect(isStandardSyntaxSelector("<% COLORS.each do |color| %>\na")).toBe( + false + ); + expect(isStandardSyntaxSelector("<% eng %>\na")).toBe(false); + }); +}); diff --git a/src/utils/hasInterpolation.js b/src/utils/hasInterpolation.js new file mode 100644 index 00000000..3dbea092 --- /dev/null +++ b/src/utils/hasInterpolation.js @@ -0,0 +1,24 @@ +import hasLessInterpolation from "./hasLessInterpolation"; +import hasPsvInterpolation from "./hasPsvInterpolation"; +import hasScssInterpolation from "./hasScssInterpolation"; +import hasTplInterpolation from "./hasTplInterpolation"; + +/** + * Check whether a string has interpolation + * + * @param {string} string + * @return {boolean} If `true`, a string has interpolation + */ +export default function(string) { + // SCSS or Less interpolation + if ( + hasLessInterpolation(string) || + hasScssInterpolation(string) || + hasTplInterpolation(string) || + hasPsvInterpolation(string) + ) { + return true; + } + + return false; +} diff --git a/src/utils/hasLessInterpolation.js b/src/utils/hasLessInterpolation.js new file mode 100644 index 00000000..832fcfb6 --- /dev/null +++ b/src/utils/hasLessInterpolation.js @@ -0,0 +1,9 @@ +/** + * Check whether a string has less interpolation + * + * @param {string} string + * @return {boolean} If `true`, a string has less interpolation + */ +export default function(string) { + return /@{.+?}/.test(string); +} diff --git a/src/utils/hasPsvInterpolation.js b/src/utils/hasPsvInterpolation.js new file mode 100644 index 00000000..edf4d32c --- /dev/null +++ b/src/utils/hasPsvInterpolation.js @@ -0,0 +1,8 @@ +/** + * Check whether a string has postcss-simple-vars interpolation + * + * @param {string} string + */ +export default function(string) { + return /\$\(.+?\)/.test(string); +} diff --git a/src/utils/hasScssInterpolation.js b/src/utils/hasScssInterpolation.js new file mode 100644 index 00000000..3eba8cb1 --- /dev/null +++ b/src/utils/hasScssInterpolation.js @@ -0,0 +1,8 @@ +/** + * Check whether a string has scss interpolation + * + * @param {string} string + */ +export default function(string) { + return /#{.+?}/.test(string); +} diff --git a/src/utils/hasTplInterpolation.js b/src/utils/hasTplInterpolation.js new file mode 100644 index 00000000..f08a6ec1 --- /dev/null +++ b/src/utils/hasTplInterpolation.js @@ -0,0 +1,9 @@ +/** + * Check whether a string has JS template literal interpolation or HTML-like template + * + * @param {string} string + * @return {boolean} If `true`, a string has template literal interpolation + */ +export default function(string) { + return /{.+?}/.test(string); +} diff --git a/src/utils/isStandardSelector.js b/src/utils/isStandardSelector.js index f622814b..a7352837 100644 --- a/src/utils/isStandardSelector.js +++ b/src/utils/isStandardSelector.js @@ -1,5 +1,5 @@ -import isStandardSyntaxSelector from "stylelint/lib/utils/isStandardSyntaxSelector"; -import hasInterpolation from "stylelint/lib/utils/hasInterpolation"; +import isStandardSyntaxSelector from "./isStandardSyntaxSelector"; +import hasInterpolation from "./hasInterpolation"; /** * Check whether a selector is standard * diff --git a/src/utils/isStandardSyntaxProperty.js b/src/utils/isStandardSyntaxProperty.js index afed3201..3d138471 100644 --- a/src/utils/isStandardSyntaxProperty.js +++ b/src/utils/isStandardSyntaxProperty.js @@ -1,11 +1,31 @@ -import isStandardSyntaxProperty from "stylelint/lib/utils/isStandardSyntaxProperty"; +import hasInterpolation from "./hasInterpolation"; /** * Check whether a property is standard * * @param {string} property - * @return {boolean} If `true`, the property is standard + * @returns {boolean} */ export default function(property) { - return isStandardSyntaxProperty(property); + // SCSS var (e.g. $var: x), list (e.g. $list: (x)) or map (e.g. $map: (key:value)) + if (property.startsWith("$")) { + return false; + } + + // Less var (e.g. @var: x) + if (property.startsWith("@")) { + return false; + } + + // Less append property value with space (e.g. transform+_: scale(2)) + if (property.endsWith("+") || property.endsWith("+_")) { + return false; + } + + // SCSS or Less interpolation + if (hasInterpolation(property)) { + return false; + } + + return true; } diff --git a/src/utils/isStandardSyntaxSelector.js b/src/utils/isStandardSyntaxSelector.js new file mode 100644 index 00000000..71baa507 --- /dev/null +++ b/src/utils/isStandardSyntaxSelector.js @@ -0,0 +1,36 @@ +import hasInterpolation from "./hasInterpolation"; + +/** + * Check whether a selector is standard + * + * @param {string} selector + * @returns {boolean} + */ +export default function(selector) { + // SCSS or Less interpolation + if (hasInterpolation(selector)) { + return false; + } + + // SCSS placeholder selectors + if (selector.startsWith("%")) { + return false; + } + + // Less :extend() + if (/:extend(\(.*?\))?/.test(selector)) { + return false; + } + + // Less mixin with resolved nested selectors (e.g. .foo().bar or .foo(@a, @b)[bar]) + if (/\.[a-z0-9-_]+\(.*\).+/i.test(selector)) { + return false; + } + + // ERB template tags + if (selector.includes("<%") || selector.includes("%>")) { + return false; + } + + return true; +}