Skip to content

Commit

Permalink
feat: no-unknown-at-rules -> no-invalid-at-rules (#12)
Browse files Browse the repository at this point in the history
* feat!: no-unknown-at-rule -> no-invalid-at-rule

* Update README

* Catch more errors in at-rules

* Adjust error reporting

* Remove unnecessary if statement

* Update README

* Fix type error

* Fix validation issues

* Fix README

* Remove unused function
  • Loading branch information
nzakas authored Nov 26, 2024
1 parent 005565e commit b90ee0e
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 139 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export default [
| :--------------------------------------------------------------- | :------------------------------- | :-------------: |
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
| [`no-unknown-at-rules`](./docs/rules/no-unknown-at-rules.md) | Disallow unknown at-rules | yes |

<!-- Rule Table End -->

Expand Down
80 changes: 80 additions & 0 deletions docs/rules/no-invalid-at-rules.md
Original file line number Diff line number Diff line change
@@ -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: "<color>";
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: "<color>";
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)
51 changes: 0 additions & 51 deletions docs/rules/no-unknown-at-rules.md

This file was deleted.

6 changes: 3 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { CSSLanguage } from "./languages/css-language.js";
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 noUnknownAtRules from "./rules/no-unknown-at-rules.js";
import noInvalidProperties from "./rules/no-invalid-properties.js";
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";

//-----------------------------------------------------------------------------
// Plugin
Expand All @@ -29,7 +29,7 @@ const plugin = {
rules: {
"no-empty-blocks": noEmptyBlocks,
"no-duplicate-imports": noDuplicateImports,
"no-unknown-at-rules": noUnknownAtRules,
"no-invalid-at-rules": noInvalidAtRules,
"no-invalid-properties": noInvalidProperties,
},
configs: {},
Expand All @@ -41,7 +41,7 @@ Object.assign(plugin.configs, {
rules: {
"css/no-empty-blocks": "error",
"css/no-duplicate-imports": "error",
"css/no-unknown-at-rules": "error",
"css/no-invalid-at-rules": "error",
"css/no-invalid-properties": "error",
},
},
Expand Down
155 changes: 155 additions & 0 deletions src/rules/no-invalid-at-rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @fileoverview Rule to prevent the use of unknown at-rules in CSS.
* @author Nicholas C. Zakas
*/

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------

import { lexer } from "css-tree";
import { isSyntaxMatchError } from "../util.js";

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/**
* Extracts metadata from an error object.
* @param {SyntaxError} error The error object to extract metadata from.
* @returns {Object} The metadata extracted from the error.
*/
function extractMetaDataFromError(error) {
const message = error.message;
const atRuleName = /`@(.*)`/u.exec(message)[1];
let messageId = "unknownAtRule";

if (message.endsWith("prelude")) {
messageId = message.includes("should not")
? "invalidExtraPrelude"
: "missingPrelude";
}

return {
messageId,
data: {
name: atRuleName,
},
};
}

//-----------------------------------------------------------------------------
// 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}}.",
invalidExtraPrelude:
"At-rule '@{{name}}' should not contain a prelude.",
missingPrelude: "At-rule '@{{name}}' should contain a prelude.",
},
},

create(context) {
const { sourceCode } = context;

return {
Atrule(node) {
// checks both name and prelude
const { error } = lexer.matchAtrulePrelude(
node.name,
node.prelude,
);

if (error) {
if (isSyntaxMatchError(error)) {
context.report({
loc: error.loc,
messageId: "invalidPrelude",
data: {
name: node.name,
prelude: error.css,
expected: error.syntax,
},
});
return;
}

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,
},
},
...extractMetaDataFromError(error),
});
}
},

"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 (isSyntaxMatchError(error)) {
context.report({
loc: error.loc,
messageId: "invalidDescriptor",
data: {
name: atRule.name,
descriptor: node.property,
value: error.css,
expected: error.syntax,
},
});
return;
}

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: node.property,
},
});
}
},
};
},
};
20 changes: 1 addition & 19 deletions src/rules/no-invalid-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,7 @@
//-----------------------------------------------------------------------------

import { lexer } from "css-tree";

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/** @typedef {import("css-tree").SyntaxMatchError} SyntaxMatchError */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/**
* Determines if an error is a syntax match error.
* @param {Object} error The error object from the CSS parser.
* @returns {error is SyntaxMatchError} True if the error is a syntax match error, false if not.
*/
function isSyntaxMatchError(error) {
return typeof error.css === "string";
}
import { isSyntaxMatchError } from "../util.js";

//-----------------------------------------------------------------------------
// Rule Definition
Expand Down
Loading

0 comments on commit b90ee0e

Please sign in to comment.