From 56e78b9ef711061a5153ff9160755de743b474c5 Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Mon, 28 Mar 2022 16:11:34 +0300 Subject: [PATCH] chore: Eslint custom plugin to warn about hex and literal colors (#19239) * wip * Add eslint custom plugin * Refactor * Clean up * Update superset-frontend/buildtools/eslint-plugin-theme-colors/index.js Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Refactor * Update superset-frontend/buildtools/eslint-plugin-theme-colors/index.js Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Clean up Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> (cherry picked from commit 6b9113a17b1cf73e5374e83a97ef17e5e440dd74) --- superset-frontend/.eslintrc.js | 20 +- superset-frontend/package-lock.json | 35 ++++ superset-frontend/package.json | 1 + .../eslint-plugin-theme-colors/colors.js | 172 ++++++++++++++++++ .../tools/eslint-plugin-theme-colors/index.js | 114 ++++++++++++ .../eslint-plugin-theme-colors/package.json | 17 ++ 6 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 superset-frontend/tools/eslint-plugin-theme-colors/colors.js create mode 100644 superset-frontend/tools/eslint-plugin-theme-colors/index.js create mode 100644 superset-frontend/tools/eslint-plugin-theme-colors/package.json diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index a1d1102694664..1ba0436c283c7 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -67,7 +67,7 @@ module.exports = { version: 'detect', }, }, - plugins: ['prettier', 'react', 'file-progress'], + plugins: ['prettier', 'react', 'file-progress', 'theme-colors'], overrides: [ { files: ['*.ts', '*.tsx'], @@ -181,10 +181,28 @@ module.exports = { ], 'no-only-tests/no-only-tests': 'error', 'max-classes-per-file': 0, + 'theme-colors/no-literal-colors': 0, + }, + }, + { + files: [ + '*.test.ts', + '*.test.tsx', + '*.test.js', + '*.test.jsx', + '*.stories.tsx', + '*.stories.jsx', + 'fixtures.*', + 'cypress-base/cypress/**/*', + 'Stories.tsx', + ], + rules: { + 'theme-colors/no-literal-colors': 0, }, }, ], rules: { + 'theme-colors/no-literal-colors': 1, camelcase: [ 'error', { diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 14bdcd2560be0..5c5a010a9cf34 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -233,6 +233,7 @@ "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^3.10.1", + "eslint-plugin-theme-colors": "file:tools/eslint-plugin-theme-colors", "exports-loader": "^0.7.0", "fetch-mock": "^7.7.3", "fork-ts-checker-webpack-plugin": "^6.3.3", @@ -278,6 +279,18 @@ "npm": "^7.5.4" } }, + "buildtools/eslint-plugin-theme-colors": { + "version": "1.0.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": "^16.9.1", + "npm": "^7.5.4" + } + }, "node_modules/@actions/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", @@ -33123,6 +33136,10 @@ "node": ">=10" } }, + "node_modules/eslint-plugin-theme-colors": { + "resolved": "tools/eslint-plugin-theme-colors", + "link": true + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -60000,6 +60017,18 @@ "src": { "version": "0.0.1", "extraneous": true + }, + "tools/eslint-plugin-theme-colors": { + "version": "1.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": "^16.9.1", + "npm": "^7.5.4" + } } }, "dependencies": { @@ -86204,6 +86233,12 @@ } } }, + "eslint-plugin-theme-colors": { + "version": "file:tools/eslint-plugin-theme-colors", + "requires": { + "lodash": "^4.17.21" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 283970b5e930e..b33a6ebfff48a 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -293,6 +293,7 @@ "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^3.10.1", + "eslint-plugin-theme-colors": "file:tools/eslint-plugin-theme-colors", "exports-loader": "^0.7.0", "fetch-mock": "^7.7.3", "fork-ts-checker-webpack-plugin": "^6.3.3", diff --git a/superset-frontend/tools/eslint-plugin-theme-colors/colors.js b/superset-frontend/tools/eslint-plugin-theme-colors/colors.js new file mode 100644 index 0000000000000..7c4d3a1a5d25c --- /dev/null +++ b/superset-frontend/tools/eslint-plugin-theme-colors/colors.js @@ -0,0 +1,172 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// https://www.w3.org/wiki/CSS/Properties/color/keywords +module.exports = [ + 'black', + 'silver', + 'gray', + 'grey', + 'white', + 'maroon', + 'red', + 'purple', + 'fuchsia', + 'green', + 'lime', + 'olive', + 'yellow', + 'navy', + 'blue', + 'teal', + 'aqua', + 'aliceblue', + 'antiquewhite', + 'aquamarine', + 'azure', + 'beige', + 'bisque', + 'blanchedalmond', + 'blueviolet', + 'brown', + 'burlywood', + 'cadetblue', + 'chartreuse', + 'chocolate', + 'coral', + 'cornflowerblue', + 'cornsilk', + 'crimson', + 'cyan', + 'darkblue', + 'darkcyan', + 'darkgoldenrod', + 'darkgray', + 'darkgreen', + 'darkgrey', + 'darkkhaki', + 'darkmagenta', + 'darkolivegreen', + 'darkorange', + 'darkorchid', + 'darkred', + 'darksalmon', + 'darkseagreen', + 'darkslateblue', + 'darkslategray', + 'darkslategrey', + 'darkturquoise', + 'darkviolet', + 'deeppink', + 'deepskyblue', + 'dimgray', + 'dimgrey', + 'dodgerblue', + 'firebrick', + 'floralwhite', + 'forestgreen', + 'fuchsia', + 'gainsboro', + 'ghostwhite', + 'gold', + 'goldenrod', + 'greenyellow', + 'honeydew', + 'hotpink', + 'indianred', + 'indigo', + 'ivory', + 'khaki', + 'lavender', + 'lavenderblush', + 'lawngreen', + 'lemonchiffon', + 'lightblue', + 'lightcoral', + 'lightcyan', + 'lightgoldenrodyellow', + 'lightgray', + 'lightgreen', + 'lightgrey', + 'lightpink', + 'lightsalmon', + 'lightseagreen', + 'lightskyblue', + 'lightslategray', + 'lightslategrey', + 'lightsteelblue', + 'lightyellow', + 'limegreen', + 'linen', + 'magenta', + 'maroon', + 'mediumaquamarine', + 'mediumblue', + 'mediumorchid', + 'mediumpurple', + 'mediumseagreen', + 'mediumslateblue', + 'mediumspringgreen', + 'mediumturquoise', + 'mediumvioletred', + 'midnightblue', + 'mintcream', + 'mistyrose', + 'moccasin', + 'navajowhite', + 'oldlace', + 'olivedrab', + 'orange', + 'orangered', + 'orchid', + 'palegoldenrod', + 'palegreen', + 'paleturquoise', + 'palevioletred', + 'papayawhip', + 'peachpuff', + 'peru', + 'pink', + 'plum', + 'powderblue', + 'rosybrown', + 'royalblue', + 'saddlebrown', + 'salmon', + 'sandybrown', + 'seagreen', + 'seashell', + 'sienna', + 'skyblue', + 'slateblue', + 'slategray', + 'slategrey', + 'snow', + 'springgreen', + 'steelblue', + 'tan', + 'teal', + 'thistle', + 'tomato', + 'turquoise', + 'violet', + 'wheat', + 'whitesmoke', + 'yellowgreen', +]; diff --git a/superset-frontend/tools/eslint-plugin-theme-colors/index.js b/superset-frontend/tools/eslint-plugin-theme-colors/index.js new file mode 100644 index 0000000000000..ce0d492a1ce5f --- /dev/null +++ b/superset-frontend/tools/eslint-plugin-theme-colors/index.js @@ -0,0 +1,114 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * @fileoverview Rule to warn about literal colors + * @author Apache + */ + +const COLOR_KEYWORDS = require('./colors'); + +function hasHexColor(quasi) { + if (typeof quasi === 'string') { + const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi; + return !!quasi.match(regex); + } + return false; +} + +function hasRgbColor(quasi) { + if (typeof quasi === 'string') { + const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i; + return !!quasi.match(regex); + } + return false; +} + +function hasLiteralColor(quasi, strict = false) { + if (typeof quasi === 'string') { + // matches literal colors at the start or end of a CSS prop + return COLOR_KEYWORDS.some(color => { + const regexColon = new RegExp(`: ${color}`); + const regexSemicolon = new RegExp(` ${color};`); + return ( + !!quasi.match(regexColon) || + !!quasi.match(regexSemicolon) || + (strict && quasi === color) + ); + }); + } + return false; +} + +const WARNING_MESSAGE = + 'Theme color variables are preferred over rgb(a)/hex/literal colors'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + rules: { + 'no-literal-colors': { + create(context) { + const warned = []; + return { + TemplateElement(node) { + const rawValue = node?.value?.raw; + const isParentProperty = + node?.parent?.parent?.type === 'TaggedTemplateExpression'; + const loc = node?.parent?.parent?.loc; + const locId = loc && JSON.stringify(loc); + const hasWarned = warned.includes(locId); + if ( + !hasWarned && + isParentProperty && + rawValue && + (hasLiteralColor(rawValue) || + hasHexColor(rawValue) || + hasRgbColor(rawValue)) + ) { + context.report(node, loc, WARNING_MESSAGE); + warned.push(locId); + } + }, + Literal(node) { + const value = node?.value; + const isParentProperty = node?.parent?.type === 'Property'; + const locId = JSON.stringify(node.loc); + const hasWarned = warned.includes(locId); + + if ( + !hasWarned && + isParentProperty && + value && + (hasLiteralColor(value, true) || + hasHexColor(value) || + hasRgbColor(value)) + ) { + context.report(node, node.loc, WARNING_MESSAGE); + warned.push(locId); + } + }, + }; + }, + }, + }, +}; diff --git a/superset-frontend/tools/eslint-plugin-theme-colors/package.json b/superset-frontend/tools/eslint-plugin-theme-colors/package.json new file mode 100644 index 0000000000000..6832811e8a386 --- /dev/null +++ b/superset-frontend/tools/eslint-plugin-theme-colors/package.json @@ -0,0 +1,17 @@ +{ + "name": "eslint-plugin-theme-colors", + "version": "1.0.0", + "description": "Warns about rgb(a)/hex/literal colors", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Apache", + "dependencies": {}, + "engines": { + "node": "^16.9.1", + "npm": "^7.5.4" + } +}