From ad53575c1a4943f9ffae6a643fc881dca4e21e14 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 19 Nov 2024 12:10:13 -0500 Subject: [PATCH] feat!: no-unknown-at-rule -> no-invalid-at-rule --- docs/rules/no-invalid-at-rules.md | 80 +++++++++++ docs/rules/no-unknown-at-rules.md | 51 ------- src/index.js | 2 +- src/rules/no-invalid-at-rules.js | 126 ++++++++++++++++++ src/rules/no-unknown-at-rules.js | 61 --------- ...es.test.js => no-invalid-at-rules.test.js} | 56 +++++++- 6 files changed, 259 insertions(+), 117 deletions(-) create mode 100644 docs/rules/no-invalid-at-rules.md delete mode 100644 docs/rules/no-unknown-at-rules.md create mode 100644 src/rules/no-invalid-at-rules.js delete mode 100644 src/rules/no-unknown-at-rules.js rename tests/rules/{no-unknown-at-rules.test.js => no-invalid-at-rules.test.js} (64%) diff --git a/docs/rules/no-invalid-at-rules.md b/docs/rules/no-invalid-at-rules.md new file mode 100644 index 0000000..b438883 --- /dev/null +++ b/docs/rules/no-invalid-at-rules.md @@ -0,0 +1,80 @@ +# no-invalid-at-rules + +Disallow invalid at-rules. + +## Background + +CSS contains a number of at-rules, each beginning with a `@`, that perform various operations. Some common at-rules include: + +- `@import` +- `@media` +- `@font-face` +- `@keyframes` +- `@supports` +- `@namespace` +- `@page` +- `@charset` + +It's important to use a known at-rule because unknown at-rules cause the browser to ignore the entire block, including any rules contained within. For example: + +```css +/* typo */ +@charse "UTF-8"; +``` + +Here, the `@charset` at-rule is incorrectly spelled as `@charse`, which means that it will be ignored. + +Each at-rule also has a defined prelude (which may be empty) and potentially one or more descriptors. For example: + +```css +@property --main-bg-color { + syntax: ""; + inherits: false; + initial-value: #000000; +} +``` + +Here, `--main-bg-color` is the prelude for `@property` while `syntax`, `inherits`, and `initial-value` are descriptors. The `@property` at-rule requires a specific format for its prelude and only specific descriptors to be present. If any of these are incorrect, the browser ignores the at-rule. + +## Rule Details + +This rule warns when it finds a CSS at-rule that is unknown or invalid according to the CSS specification. As such, the rule warns for the following problems: + +- An unknown at-rule +- An invalid prelude for a known at-rule +- An unknown descriptor for a known at-rule +- An invalid descriptor value for a known at-rule + +The at-rule data is provided via the [CSSTree](https://github.com/csstree/csstree) project. + +Examples of incorrect code: + +```css +@charse "UTF-8"; + +@importx url(foo.css); + +@foobar { + .my-style { + color: red; + } +} + +@property main-bg-color { + syntax: ""; + inherits: false; + initial-value: #000000; +} + +@property --main-bg-color { + syntax: red; +} +``` + +## When Not to Use It + +If you are purposely using at-rules that aren't part of the CSS specification, then you can safely disable this rule. + +## Prior Art + +- [`at-rule-no-unknown`](https://stylelint.io/user-guide/rules/at-rule-no-unknown) diff --git a/docs/rules/no-unknown-at-rules.md b/docs/rules/no-unknown-at-rules.md deleted file mode 100644 index 197a0fb..0000000 --- a/docs/rules/no-unknown-at-rules.md +++ /dev/null @@ -1,51 +0,0 @@ -# no-unknown-at-rules - -Disallow unknown at-rules. - -## Background - -CSS contains a number of at-rules, each beginning with a `@`, that perform various operations. Some common at-rules include: - -- `@import` -- `@media` -- `@font-face` -- `@keyframes` -- `@supports` -- `@namespace` -- `@page` -- `@charset` - -It's important to use a known at-rule because unknown at-rules cause the browser to ignore the entire block, including any rules contained within. For example: - -```css -/* typo */ -@charse "UTF-8"; -``` - -Here, the `@charset` at-rule is incorrectly spelled as `@charse`, which means that it will be ignored. - -## Rule Details - -This rule warns when it finds a CSS at-rule that isn't part of the CSS specification. The at-rule data is provided via the [CSSTree](https://github.com/csstree/csstree) project. - -Examples of incorrect code: - -```css -@charse "UTF-8"; - -@importx url(foo.css); - -@foobar { - .my-style { - color: red; - } -} -``` - -## When Not to Use It - -If you are purposely using at-rules that aren't part of the CSS specification, then you can safely disable this rule. - -## Prior Art - -- [`at-rule-no-unknown`](https://stylelint.io/user-guide/rules/at-rule-no-unknown) diff --git a/src/index.js b/src/index.js index 28aebc3..4277cc3 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import { CSSSourceCode } from "./languages/css-source-code.js"; import noEmptyBlocks from "./rules/no-empty-blocks.js"; import noDuplicateImports from "./rules/no-duplicate-imports.js"; import noUnknownProperties from "./rules/no-unknown-properties.js"; -import noUnknownAtRules from "./rules/no-unknown-at-rules.js"; +import noUnknownAtRules from "./rules/no-invalid-at-rules.js"; //----------------------------------------------------------------------------- // Plugin diff --git a/src/rules/no-invalid-at-rules.js b/src/rules/no-invalid-at-rules.js new file mode 100644 index 0000000..da0836f --- /dev/null +++ b/src/rules/no-invalid-at-rules.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Rule to prevent the use of unknown at-rules in CSS. + * @author Nicholas C. Zakas + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import { lexer } from "css-tree"; + +//----------------------------------------------------------------------------- +// Rule Definition +//----------------------------------------------------------------------------- + +export default { + meta: { + type: "problem", + + docs: { + description: "Disallow invalid at-rules.", + recommended: true, + }, + + messages: { + unknownAtRule: "Unknown at-rule '@{{name}}' found.", + invalidPrelude: + "Invalid prelude '{{prelude}}' found for at-rule '@{{name}}'. Expected '{{expected}}'.", + unknownDescriptor: + "Unknown descriptor '{{descriptor}}' found for at-rule '@{{name}}'.", + invalidDescriptor: + "Invalid value '{{value}}' for descriptor '{{descriptor}}' found for at-rule '@{{name}}'. Expected {{expected}}.", + }, + }, + + create(context) { + const { sourceCode } = context; + + return { + Atrule(node) { + // checks both name and prelude + const { error } = lexer.matchAtrulePrelude( + node.name, + node.prelude, + ); + + if (error) { + if (error.reference) { + const loc = node.loc; + + context.report({ + loc: { + start: loc.start, + end: { + line: loc.start.line, + + // add 1 to account for the @ symbol + column: + loc.start.column + node.name.length + 1, + }, + }, + messageId: "unknownAtRule", + data: { + name: node.name, + }, + }); + } else { + context.report({ + loc: error.loc, + messageId: "invalidPrelude", + data: { + name: node.name, + prelude: error.css, + expected: error.syntax, + }, + }); + } + } + }, + + "AtRule > Block > Declaration"(node) { + // get at rule node + const atRule = sourceCode.getParent(sourceCode.getParent(node)); + + const { error } = lexer.matchAtruleDescriptor( + atRule.name, + node.property, + node.value, + ); + + if (error) { + if (error.reference) { + const loc = node.loc; + + context.report({ + loc: { + start: loc.start, + end: { + line: loc.start.line, + column: + loc.start.column + node.property.length, + }, + }, + messageId: "unknownDescriptor", + data: { + name: atRule.name, + descriptor: error.reference, + }, + }); + } else { + context.report({ + loc: error.loc, + messageId: "invalidDescriptor", + data: { + name: atRule.name, + descriptor: node.property, + value: error.css, + expected: error.syntax, + }, + }); + } + } + }, + }; + }, +}; diff --git a/src/rules/no-unknown-at-rules.js b/src/rules/no-unknown-at-rules.js deleted file mode 100644 index 9000868..0000000 --- a/src/rules/no-unknown-at-rules.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @fileoverview Rule to prevent the use of unknown at-rules in CSS. - * @author Nicholas C. Zakas - */ - -//----------------------------------------------------------------------------- -// Imports -//----------------------------------------------------------------------------- - -import data from "css-tree/definition-syntax-data"; - -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -const knownAtRules = new Set(Object.keys(data.atrules)); - -//----------------------------------------------------------------------------- -// Rule Definition -//----------------------------------------------------------------------------- - -export default { - meta: { - type: "problem", - - docs: { - description: "Disallow unknown at-rules.", - recommended: true, - }, - - messages: { - unknownAtRule: "Unknown at-rule '{{name}}' found.", - }, - }, - - create(context) { - return { - Atrule(node) { - if (!knownAtRules.has(node.name)) { - const loc = node.loc; - - context.report({ - loc: { - start: loc.start, - end: { - line: loc.start.line, - - // add 1 to account for the @ symbol - column: loc.start.column + node.name.length + 1, - }, - }, - messageId: "unknownAtRule", - data: { - name: node.name, - }, - }); - } - }, - }; - }, -}; diff --git a/tests/rules/no-unknown-at-rules.test.js b/tests/rules/no-invalid-at-rules.test.js similarity index 64% rename from tests/rules/no-unknown-at-rules.test.js rename to tests/rules/no-invalid-at-rules.test.js index 1d2ebf4..972f728 100644 --- a/tests/rules/no-unknown-at-rules.test.js +++ b/tests/rules/no-invalid-at-rules.test.js @@ -1,5 +1,5 @@ /** - * @fileoverview Tests for no-unknown-at-rules rule. + * @fileoverview Tests for no-invalid-at-rules rule. * @author Nicholas C. Zakas */ @@ -7,7 +7,7 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-unknown-at-rules.js"; +import rule from "../../src/rules/no-invalid-at-rules.js"; import css from "../../src/index.js"; import { RuleTester } from "eslint"; @@ -22,7 +22,7 @@ const ruleTester = new RuleTester({ language: "css/css", }); -ruleTester.run("no-unknown-at-rules", rule, { +ruleTester.run("no-invalid-at-rules", rule, { valid: [ "@import url('styles.css');", "@font-face { font-family: 'MyFont'; src: url('myfont.woff2') format('woff2'); }", @@ -62,7 +62,7 @@ ruleTester.run("no-unknown-at-rules", rule, { code: "@foobaz { }", errors: [ { - message: "Unknown at-rule 'foobaz' found.", + message: "Unknown at-rule '@foobaz' found.", line: 1, column: 1, endLine: 1, @@ -91,5 +91,53 @@ ruleTester.run("no-unknown-at-rules", rule, { }, ], }, + { + code: "@property foo { }", + errors: [ + { + messageId: "invalidPrelude", + data: { + name: "property", + prelude: "foo", + expected: "", + }, + line: 1, + column: 11, + endLine: 1, + endColumn: 14, + }, + ], + }, + { + code: "@property --foo { baz: red; }", + errors: [ + { + messageId: "unknownDescriptor", + data: { name: "property", descriptor: "baz" }, + line: 1, + column: 19, + endLine: 1, + endColumn: 22, + }, + ], + }, + { + code: "@property --foo { syntax: red; }", + errors: [ + { + messageId: "invalidDescriptor", + line: 1, + data: { + name: "property", + descriptor: "syntax", + value: "red", + expected: "", + }, + column: 27, + endLine: 1, + endColumn: 30, + }, + ], + }, ], });