Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: no-unknown-at-rules -> no-invalid-at-rules #12

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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