From 96144bd9831580aca7f64853f8a9c7f3a77b9d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 24 May 2023 10:12:41 +0200 Subject: [PATCH] ci: Add rule `dangerously-use-html-string-missing` (no-changelog) (#6299) --- packages/@n8n_io/eslint-config/frontend.js | 9 ++ packages/@n8n_io/eslint-config/local-rules.js | 114 ++++++++++++++++++ packages/design-system/.eslintrc.js | 2 + 3 files changed, 125 insertions(+) diff --git a/packages/@n8n_io/eslint-config/frontend.js b/packages/@n8n_io/eslint-config/frontend.js index 9184438438..e36d4c33b7 100644 --- a/packages/@n8n_io/eslint-config/frontend.js +++ b/packages/@n8n_io/eslint-config/frontend.js @@ -14,6 +14,15 @@ module.exports = { ignorePatterns: ['**/*.js', '**/*.d.ts', 'vite.config.ts', '**/*.ts.snap'], + overrides: [ + { + files: ['src/**/*.vue'], + rules: { + 'n8n-local-rules/dangerously-use-html-string-missing': 'error', + }, + }, + ], + rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', diff --git a/packages/@n8n_io/eslint-config/local-rules.js b/packages/@n8n_io/eslint-config/local-rules.js index 60f15de392..56eb10d5d8 100644 --- a/packages/@n8n_io/eslint-config/local-rules.js +++ b/packages/@n8n_io/eslint-config/local-rules.js @@ -1,5 +1,7 @@ 'use strict'; +const path = require('path'); + /** * This file contains any locally defined ESLint rules. They are picked up by * eslint-plugin-n8n-local-rules and exposed as 'n8n-local-rules/'. @@ -239,6 +241,118 @@ module.exports = { }; }, }, + + 'dangerously-use-html-string-missing': { + meta: { + type: 'error', + docs: { + description: + 'Calls to the `showToast` and `showMessage` methods must include `dangerouslyUseHTMLString: true` when at least one of the values in `title` or `message` contains HTML.', + recommended: 'error', + /** + * @note This rule does not yet cover cases where the result of calling + * `this.$locale.someMethod()` is assigned to a variable that is then + * assigned to `title or `message`, e.g. `message: errorMessage`. + */ + }, + }, + create(context) { + const cwd = context.getCwd(); + const locale = 'src/plugins/i18n/locales/en.json'; + + const LOCALE_NAMESPACE = '$locale'; + const LOCALE_FILEPATH = cwd.endsWith('editor-ui') + ? path.join(cwd, locale) + : path.join(cwd, 'packages/editor-ui', locale); + + let LOCALE_MAP; + + try { + LOCALE_MAP = JSON.parse(require('fs').readFileSync(LOCALE_FILEPATH)); + } catch { + console.log( + '[dangerously-use-html-string-missing] Failed to load locale map, skipping rule...', + ); + return {}; + } + + const METHODS_POSSIBLY_REQUIRING_HTML = new Set(['showToast', 'showMessage']); + const PROPERTIES_POSSIBLY_CONTAINING_HTML = new Set(['title', 'message']); + const USE_HTML_PROPERTY = 'dangerouslyUseHTMLString'; + + const isMethodPossiblyRequiringRawHtml = (node) => + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'ThisExpression' && + node.callee.property.type === 'Identifier' && + METHODS_POSSIBLY_REQUIRING_HTML.has(node.callee.property.name) && + node.arguments.length === 1 && + node.arguments.at(0).type === 'ObjectExpression'; + + const isPropertyWithLocaleStringAsValue = (property) => + property.key.type === 'Identifier' && + PROPERTIES_POSSIBLY_CONTAINING_HTML.has(property.key.name) && + property.value.type === 'CallExpression' && + property.value.callee.type === 'MemberExpression' && + property.value.callee.object.type === 'MemberExpression' && + property.value.callee.object.property.type === 'Identifier' && + property.value.callee.object.property.name === LOCALE_NAMESPACE && + property.value.arguments.length >= 1 && + property.value.arguments.at(0).type === 'Literal' && + typeof property.value.arguments.at(0).value === 'string'; + + const containsHtml = (str) => { + let insideTag = false; + + for (let char of str) { + if (char === '<') { + insideTag = true; + } else if (char === '>') { + if (insideTag) return true; + insideTag = false; + } + } + + return false; + }; + + return { + CallExpression(node) { + if (!isMethodPossiblyRequiringRawHtml(node)) return; + + const arg = node.arguments.at(0); + + const hasArgWitHtml = arg.properties + .reduce( + (acc, p) => + isPropertyWithLocaleStringAsValue(p) + ? [...acc, p.value.arguments.at(0).value] + : acc, + [], + ) + .some((i) => containsHtml(LOCALE_MAP[i])); + + if (!hasArgWitHtml) return; + + const hasRawHtmlPropertyAsTrue = arg.properties.some( + (p) => + p.key.type === 'Identifier' && + p.key.name === USE_HTML_PROPERTY && + p.value.type === 'Literal' && + p.value.value === true, + ); + + if (hasRawHtmlPropertyAsTrue) return; + + const methodName = node.callee.property.name; + + context.report({ + node, + message: `Set \`${USE_HTML_PROPERTY}: true\` in the argument to \`${methodName}\`. At least one of the values in \`title\` or \`message\` contains HTML.`, + }); + }, + }; + }, + }, }; const isJsonParseCall = (node) => diff --git a/packages/design-system/.eslintrc.js b/packages/design-system/.eslintrc.js index 4b59d8f40c..4e93b1a773 100644 --- a/packages/design-system/.eslintrc.js +++ b/packages/design-system/.eslintrc.js @@ -9,6 +9,8 @@ module.exports = { ...sharedOptions(__dirname, 'frontend'), rules: { + 'n8n-local-rules/dangerously-use-html-string-missing': 'off', + // TODO: Remove these 'import/no-default-export': 'off', 'import/order': 'off',