From 26a62b835e2b7388ec9c9de32c40da7c399f115f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Wed, 20 Jul 2022 22:07:01 +0200 Subject: [PATCH] feat: support external HTML plugins --- package-lock.json | 32 ++++++ package.json | 2 + .../other-html-plugins-compatibility.html | 4 + src/__tests__/plugin.js | 98 +++++++++++++++---- src/index.js | 86 +++++++++++++--- 5 files changed, 192 insertions(+), 30 deletions(-) create mode 100644 src/__tests__/fixtures/other-html-plugins-compatibility.html diff --git a/package-lock.json b/package-lock.json index c97c234..96ad0a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "htmlparser2": "^8.0.1" }, "devDependencies": { + "@html-eslint/eslint-plugin": "^0.13.2", + "@html-eslint/parser": "^0.13.2", "eslint": "^8.5.0", "eslint-config-prettier": "^8.5.0", "jest": "^28.1.3", @@ -620,6 +622,24 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@html-eslint/eslint-plugin": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.13.2.tgz", + "integrity": "sha512-44r088HnispQAAT7Mfb+tCy/T7qEAhDb313KsOSUIFwOgw8A2ITLMZRoZWDJjYgyAMnp1FovTB4A+ujKD11RKA==", + "dev": true, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@html-eslint/parser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.13.2.tgz", + "integrity": "sha512-DESYDCWOo8JkpLCLpSgoTd437ReTxA955T//dGm7+dnW1UnVs6M2Axpd40smF7ucyBqjTUj7YaE/eMRjlLMoPg==", + "dev": true, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -4875,6 +4895,18 @@ "strip-json-comments": "^3.1.1" } }, + "@html-eslint/eslint-plugin": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.13.2.tgz", + "integrity": "sha512-44r088HnispQAAT7Mfb+tCy/T7qEAhDb313KsOSUIFwOgw8A2ITLMZRoZWDJjYgyAMnp1FovTB4A+ujKD11RKA==", + "dev": true + }, + "@html-eslint/parser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.13.2.tgz", + "integrity": "sha512-DESYDCWOo8JkpLCLpSgoTd437ReTxA955T//dGm7+dnW1UnVs6M2Axpd40smF7ucyBqjTUj7YaE/eMRjlLMoPg==", + "dev": true + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", diff --git a/package.json b/package.json index 92d9a0d..331037e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "htmlparser2": "^8.0.1" }, "devDependencies": { + "@html-eslint/eslint-plugin": "^0.13.2", + "@html-eslint/parser": "^0.13.2", "eslint": "^8.5.0", "eslint-config-prettier": "^8.5.0", "jest": "^28.1.3", diff --git a/src/__tests__/fixtures/other-html-plugins-compatibility.html b/src/__tests__/fixtures/other-html-plugins-compatibility.html new file mode 100644 index 0000000..1e70844 --- /dev/null +++ b/src/__tests__/fixtures/other-html-plugins-compatibility.html @@ -0,0 +1,4 @@ + + diff --git a/src/__tests__/plugin.js b/src/__tests__/plugin.js index 318eff0..8705fe8 100644 --- a/src/__tests__/plugin.js +++ b/src/__tests__/plugin.js @@ -4,7 +4,7 @@ const path = require("path") const eslint = require("eslint") const semver = require("semver") const eslintVersion = require("eslint/package.json").version -const plugin = require("..") +require("..") function matchVersion(versionSpec) { return semver.satisfies(eslintVersion, versionSpec, { @@ -17,48 +17,49 @@ function ifVersion(versionSpec, fn, ...args) { execFn(...args) } -async function execute(file, baseConfig) { - if (!baseConfig) baseConfig = {} - +async function execute(file, options = {}) { const files = [path.join(__dirname, "fixtures", file)] - const options = { + const eslintOptions = { extensions: ["html"], baseConfig: { - settings: baseConfig.settings, + settings: options.settings, rules: Object.assign( { "no-console": 2, }, - baseConfig.rules + options.rules ), - globals: baseConfig.globals, - env: baseConfig.env, - parserOptions: baseConfig.parserOptions, + globals: options.globals, + env: options.env, + parserOptions: options.parserOptions, + parser: options.parser, }, ignore: false, useEslintrc: false, - fix: baseConfig.fix, + fix: options.fix, reportUnusedDisableDirectives: - baseConfig.reportUnusedDisableDirectives || null, + options.reportUnusedDisableDirectives || null, } let results if (eslint.ESLint) { - const instance = new eslint.ESLint({ - ...options, - plugins: { html: plugin }, - }) + eslintOptions.baseConfig.plugins = options.plugins + const instance = new eslint.ESLint(eslintOptions) results = (await instance.lintFiles(files))[0] } else if (eslint.CLIEngine) { - const cli = new eslint.CLIEngine(options) - cli.addPlugin("html", plugin) + const cli = new eslint.CLIEngine(eslintOptions) + if (options.plugins) { + for (const plugin of options.plugins) { + cli.addPlugin(plugin.split("/")[0], require(plugin)) + } + } results = cli.executeOnFiles(files).results[0] } else { throw new Error("invalid ESLint dependency") } - return baseConfig.fix ? results : results && results.messages + return options.fix ? results : results && results.messages } it("should extract and remap messages", async () => { @@ -790,3 +791,62 @@ describe("scope sharing", () => { expect(messages[15].message).toBe("'ClassGloballyDeclared' is not defined.") }) }) + +// For some reason @html-eslint is not compatible with ESLint < 5 +ifVersion(">= 5", describe, "compatibility with external HTML plugins", () => { + it("check", async () => { + const messages = await execute("other-html-plugins-compatibility.html", { + plugins: ["@html-eslint/eslint-plugin"], + parser: "@html-eslint/parser", + rules: { + "@html-eslint/require-img-alt": ["error"], + }, + }) + expect(messages).toMatchInlineSnapshot(` + Array [ + Object { + "column": 1, + "endColumn": 13, + "endLine": 1, + "line": 1, + "message": "Missing \`alt\` attribute at \`\` tag", + "messageId": "missingAlt", + "nodeType": null, + "ruleId": "@html-eslint/require-img-alt", + "severity": 2, + }, + Object { + "column": 3, + "endColumn": 14, + "endLine": 3, + "line": 3, + "message": "Unexpected console statement.", + "messageId": "unexpected", + "nodeType": "MemberExpression", + "ruleId": "no-console", + "severity": 2, + "source": " console.log(\\"toto\\")", + }, + ] + `) + }) + + it("fix", async () => { + const result = await execute("other-html-plugins-compatibility.html", { + plugins: ["@html-eslint/eslint-plugin"], + parser: "@html-eslint/parser", + rules: { + "@html-eslint/quotes": ["error", "single"], + quotes: ["error", "single"], + }, + fix: true, + }) + expect(result.output).toMatchInlineSnapshot(` + " + + " + `) + }) +}) diff --git a/src/index.js b/src/index.js index 4767e58..059d770 100644 --- a/src/index.js +++ b/src/index.js @@ -126,22 +126,37 @@ function patch(Linter) { filenameOrOptions, saveState ) { + const callOriginalVerify = () => + verify.call(this, textOrSourceCode, config, filenameOrOptions, saveState) + if (typeof config.extractConfig === "function") { - return verify.call(this, textOrSourceCode, config, filenameOrOptions) + return callOriginalVerify() } const pluginSettings = getSettings(config.settings || {}) const mode = getFileMode(pluginSettings, filenameOrOptions) if (!mode || typeof textOrSourceCode !== "string") { - return verify.call( - this, - textOrSourceCode, - config, - filenameOrOptions, - saveState - ) + return callOriginalVerify() + } + + let messages + ;[messages, config] = verifyExternalHtmlPlugin(config, callOriginalVerify) + + if (config.parser && config.parser.id === "@html-eslint/parser") { + messages.push(...callOriginalVerify()) + const rules = {} + for (const name in config.rules) { + if (!name.startsWith("@html-eslint/")) { + rules[name] = config.rules[name] + } + } + config = editConfig(config, { + parser: null, + rules, + }) } + const extractResult = extract( textOrSourceCode, pluginSettings.indent, @@ -150,8 +165,6 @@ function patch(Linter) { pluginSettings.isJavaScriptMIMEType ) - const messages = [] - if (pluginSettings.reportBadIndent) { messages.push( ...extractResult.badIndentationLines.map((line) => ({ @@ -181,7 +194,7 @@ function patch(Linter) { const localMessages = verify.call( this, sourceCodes.get(codePart) || String(codePart), - Object.assign({}, config, { + editConfig(config, { rules: Object.assign( { [PREPARE_RULE_NAME]: "error" }, !ignoreRules && config.rules @@ -215,6 +228,57 @@ function patch(Linter) { } } +function editConfig(config, { parser = config.parser, rules = config.rules }) { + return { + ...config, + parser, + rules, + } +} + +const externalHtmlPluginPrefixes = [ + "@html-eslint/", + "@angular-eslint/template-", +] + +function getParserId(config) { + if (!config.parser) { + return + } + + if (typeof config.parser === "string") { + // old versions of ESLint (ex: 4.7) + return config.parser + } + + return config.parser.id +} + +function verifyExternalHtmlPlugin(config, callOriginalVerify) { + const parserId = getParserId(config) + const externalHtmlPluginPrefix = + parserId && + externalHtmlPluginPrefixes.find((prefix) => parserId.startsWith(prefix)) + if (!externalHtmlPluginPrefix) { + return [[], config] + } + + const rules = {} + for (const name in config.rules) { + if (!name.startsWith(externalHtmlPluginPrefix)) { + rules[name] = config.rules[name] + } + } + + return [ + callOriginalVerify(), + editConfig(config, { + parser: null, + rules, + }), + ] +} + function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) { // First pass: collect needed globals and declared globals for each script tags. const firstPassValues = []