From 7128750e932d6a0507db62924c0be0a58216ff35 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Nov 2024 11:48:38 -0500 Subject: [PATCH] Catch more errors in at-rules --- src/index.js | 6 +- src/rules/no-invalid-at-rules.js | 112 +++++++++++++++--------- src/rules/no-invalid-properties.js | 20 +---- src/util.js | 33 +++++++ tests/rules/no-invalid-at-rules.test.js | 26 ++++++ 5 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 src/util.js diff --git a/src/index.js b/src/index.js index a130917..fe8bd2c 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 noInvalidProperties from "./rules/no-invalid-properties.js"; -import noUnknownAtRules from "./rules/no-invalid-at-rules.js"; +import noInvalidAtRules from "./rules/no-invalid-at-rules.js"; //----------------------------------------------------------------------------- // Plugin @@ -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: {}, @@ -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", }, }, diff --git a/src/rules/no-invalid-at-rules.js b/src/rules/no-invalid-at-rules.js index da0836f..17f099c 100644 --- a/src/rules/no-invalid-at-rules.js +++ b/src/rules/no-invalid-at-rules.js @@ -8,6 +8,35 @@ //----------------------------------------------------------------------------- import { lexer } from "css-tree"; +import { isSyntaxReferenceError, 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 @@ -30,6 +59,9 @@ export default { "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.", }, }, @@ -45,26 +77,7 @@ export default { ); 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 { + if (isSyntaxMatchError(error)) { context.report({ loc: error.loc, messageId: "invalidPrelude", @@ -74,7 +87,23 @@ export default { 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), + }); } }, @@ -89,25 +118,7 @@ export default { ); 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 { + if (isSyntaxMatchError(error)) { context.report({ loc: error.loc, messageId: "invalidDescriptor", @@ -118,7 +129,30 @@ export default { expected: error.syntax, }, }); + return; } + + const loc = node.loc; + const metaData = isSyntaxReferenceError(error) + ? { + messageId: "unknownDescriptor", + data: { + name: atRule.name, + descriptor: error.reference, + }, + } + : extractMetaDataFromError(error); + + context.report({ + loc: { + start: loc.start, + end: { + line: loc.start.line, + column: loc.start.column + node.property.length, + }, + }, + ...metaData, + }); } }, }; diff --git a/src/rules/no-invalid-properties.js b/src/rules/no-invalid-properties.js index 4fdaaeb..ec01339 100644 --- a/src/rules/no-invalid-properties.js +++ b/src/rules/no-invalid-properties.js @@ -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 diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..12e7e12 --- /dev/null +++ b/src/util.js @@ -0,0 +1,33 @@ +/** + * @fileoverview Utility functions for ESLint CSS plugin. + * @author Nicholas C. Zakas + */ + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {import("css-tree").SyntaxReferenceError} SyntaxReferenceError */ +/** @typedef {import("css-tree").SyntaxMatchError} SyntaxMatchError */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Determines if an error is a reference error. + * @param {Object} error The error object to check. + * @returns {error is SyntaxReferenceError} True if the error is a reference error, false if not. + */ +export function isSyntaxReferenceError(error) { + return typeof error.reference === "string"; +} + +/** + * Determines if an error is a syntax match error. + * @param {Object} error The error object to check. + * @returns {error is SyntaxMatchError} True if the error is a syntax match error, false if not. + */ +export function isSyntaxMatchError(error) { + return typeof error.syntax === "string"; +} diff --git a/tests/rules/no-invalid-at-rules.test.js b/tests/rules/no-invalid-at-rules.test.js index 972f728..8e903c0 100644 --- a/tests/rules/no-invalid-at-rules.test.js +++ b/tests/rules/no-invalid-at-rules.test.js @@ -139,5 +139,31 @@ ruleTester.run("no-invalid-at-rules", rule, { }, ], }, + { + code: "@supports { a {} }", + errors: [ + { + messageId: "missingPrelude", + data: { name: "supports" }, + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + code: "@font-face foo { }", + errors: [ + { + messageId: "invalidExtraPrelude", + data: { name: "font-face" }, + line: 1, + column: 1, + endLine: 1, + endColumn: 11, + }, + ], + }, ], });