From f7f92e14130541d25b6838227e492ef93f353b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Fri, 26 Apr 2024 20:50:47 +0200 Subject: [PATCH 1/2] Install `css-validator` ` --- package-lock.json | 199 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 197 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 762f5c1..3824c1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.11.0", "license": "MIT", "dependencies": { + "csstree-validator": "^3.0.0", "parse5": "^6.0.1", "parse5-htmlparser2-tree-adapter": "^6.0.1", "requireindex": "^1.2.0" @@ -1201,7 +1202,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, "engines": { "node": ">=6" } @@ -1465,6 +1465,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/clap": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/clap/-/clap-3.1.1.tgz", + "integrity": "sha512-vp42956Ax06WwaaheYEqEOgXZ3VKJxgccZ0gJL0HpyiupkIS9RVJFo5eDU1BPeQAOqz+cclndZg4DCqG1sJReQ==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1541,6 +1553,35 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/csstree-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csstree-validator/-/csstree-validator-3.0.0.tgz", + "integrity": "sha512-Y5OSq3wI0Xz6L7DCgJQtQ97U+v99SkX9r663VjpvUMJPhEr0A149OxiAGqcnokB5bt81irgnMudspBzujzqn0w==", + "dependencies": { + "clap": "^3.0.0", + "css-tree": "^2.0.2", + "resolve": "^1.20.0" + }, + "bin": { + "csstree-validator": "bin/validate.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2301,6 +2342,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2432,6 +2481,17 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2524,6 +2584,17 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3010,6 +3081,11 @@ "semver": "bin/semver.js" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3634,6 +3710,11 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", @@ -3828,6 +3909,22 @@ "node": ">=0.10.5" } }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4055,6 +4152,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -4180,6 +4285,17 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5418,8 +5534,7 @@ "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" }, "ansi-regex": { "version": "5.0.1", @@ -5613,6 +5728,14 @@ "readdirp": "~3.6.0" } }, + "clap": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/clap/-/clap-3.1.1.tgz", + "integrity": "sha512-vp42956Ax06WwaaheYEqEOgXZ3VKJxgccZ0gJL0HpyiupkIS9RVJFo5eDU1BPeQAOqz+cclndZg4DCqG1sJReQ==", + "requires": { + "ansi-colors": "^4.1.1" + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -5683,6 +5806,25 @@ "which": "^2.0.1" } }, + "css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "requires": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + } + }, + "csstree-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csstree-validator/-/csstree-validator-3.0.0.tgz", + "integrity": "sha512-Y5OSq3wI0Xz6L7DCgJQtQ97U+v99SkX9r663VjpvUMJPhEr0A149OxiAGqcnokB5bt81irgnMudspBzujzqn0w==", + "requires": { + "clap": "^3.0.0", + "css-tree": "^2.0.2", + "resolve": "^1.20.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6238,6 +6380,11 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6333,6 +6480,14 @@ "type-fest": "^0.8.0" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -6404,6 +6559,14 @@ "binary-extensions": "^2.0.0" } }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "requires": { + "hasown": "^2.0.0" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6768,6 +6931,11 @@ } } }, + "mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7239,6 +7407,11 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", @@ -7367,6 +7540,16 @@ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==" }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7518,6 +7701,11 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -7610,6 +7798,11 @@ "has-flag": "^3.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index a728a06..07ac255 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "node": ">= 12" }, "dependencies": { + "csstree-validator": "^3.0.0", "parse5": "^6.0.1", "parse5-htmlparser2-tree-adapter": "^6.0.1", "requireindex": "^1.2.0" From c2c1969f14225eedbfecb53ed1af5c726de6c5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Fri, 26 Apr 2024 20:55:44 +0200 Subject: [PATCH 2/2] Implement `no-invalid-css` rule --- custom_types/csstree-validator.d.ts | 1 + docs/rules/no-invalid-css.md | 30 ++++++++++++ src/rules/no-invalid-css.ts | 60 +++++++++++++++++++++++ src/test/rules/no-invalid-css_test.ts | 69 +++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 custom_types/csstree-validator.d.ts create mode 100644 docs/rules/no-invalid-css.md create mode 100644 src/rules/no-invalid-css.ts create mode 100644 src/test/rules/no-invalid-css_test.ts diff --git a/custom_types/csstree-validator.d.ts b/custom_types/csstree-validator.d.ts new file mode 100644 index 0000000..0b3eff6 --- /dev/null +++ b/custom_types/csstree-validator.d.ts @@ -0,0 +1 @@ +declare module 'csstree-validator'; diff --git a/docs/rules/no-invalid-css.md b/docs/rules/no-invalid-css.md new file mode 100644 index 0000000..7a5dadf --- /dev/null +++ b/docs/rules/no-invalid-css.md @@ -0,0 +1,30 @@ +# Disallows invalid CSS in templates (no-invalid-css) + +Templates should all contain valid CSS, if any, as it is expected +to be parsed as part of rendering. + +## Rule Details + +This rule disallows invalid CSS in templates. + +The following patterns are considered warnings: + +```ts +css`foo bar`; +css`.footer { 24px; color: blue; }`; +``` + +The following patterns are not warnings: + +```ts +css` + .header { + margin: 24px; + color: blue; + } +`; +``` + +## When Not To Use It + +If you don't care about invalid CSS, then you will not need this rule. diff --git a/src/rules/no-invalid-css.ts b/src/rules/no-invalid-css.ts new file mode 100644 index 0000000..9258ca3 --- /dev/null +++ b/src/rules/no-invalid-css.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview Disallows invalid CSS in templates + * @author Kristján Oddsson + */ + +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {validate as validateCSS} from 'csstree-validator'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Disallows invalid CSS in templates', + recommended: false, + url: 'https://github.com/43081j/eslint-plugin-lit/blob/master/docs/rules/no-invalid-css.md' + }, + schema: [], + messages: { + parseError: 'Template contained invalid CSS syntax, error was: {{ err }}' + } + }, + + create(context): Rule.RuleListener { + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + TaggedTemplateExpression: (node: ESTree.Node): void => { + if ( + node.type === 'TaggedTemplateExpression' && + node.tag.type === 'Identifier' && + node.tag.name === 'css' + ) { + const errors = validateCSS( + node.quasi.quasis.map((quasi) => quasi.value.raw).join('') + ); + + for (const err of errors) { + context.report({ + loc: {line: err.line, column: err.column}, + messageId: 'parseError', + data: {err: err.formattedMessage} + }); + } + } + } + }; + } +}; + +export = rule; diff --git a/src/test/rules/no-invalid-css_test.ts b/src/test/rules/no-invalid-css_test.ts new file mode 100644 index 0000000..d47a1ba --- /dev/null +++ b/src/test/rules/no-invalid-css_test.ts @@ -0,0 +1,69 @@ +/** + * @fileoverview Disallows invalid CSS in templates + * @author Kristján Oddsson + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule = require('../../rules/no-invalid-css'); +import {RuleTester} from 'eslint'; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2015 + } +}); + +ruleTester.run('no-invalid-css', rule, { + valid: ['css`.foobar { margin: 0 10px; }`'], + + invalid: [ + { + code: 'css`foo bar`', + errors: [ + { + line: 1, + column: 9, + messageId: 'parseError' + } + ] + }, + { + code: 'css`.footer { 24px; color: blue; }`', + errors: [ + { + line: 1, + column: 12, + messageId: 'parseError' + } + ] + }, + { + code: 'css`.footer { margin: 24px color: blue; }`', + errors: [ + { + line: 1, + column: 25, + messageId: 'parseError' + } + ] + }, + { + code: 'css`.footer { magin: 24px; color: blue; }`', + errors: [ + { + line: 1, + column: 12, + messageId: 'parseError' + } + ] + } + ] +});