"use strict" const path = require("path") const extract = require("./extract") const utils = require("./utils") const splatSet = utils.splatSet const getSettings = require("./settings").getSettings const BOM = "\uFEFF" const GET_SCOPE_RULE_NAME = "__eslint-plugin-html-get-scope" const DECLARE_VARIABLES_RULE_NAME = "__eslint-plugin-html-declare-variables" const LINTER_ISPATCHED_PROPERTY_NAME = "__eslint-plugin-html-verify-function-is-patched" // Disclaimer: // // This is not a long term viable solution. ESLint needs to improve its processor API to // provide access to the configuration before actually preprocess files, but it's not // planed yet. This solution is quite ugly but shouldn't alter eslint process. // // Related github issues: // https://github.com/eslint/eslint/issues/3422 // https://github.com/eslint/eslint/issues/4153 const needle = path.join("lib", "linter.js") iterateESLintModules(patch) function getModuleFromRequire() { return require("eslint/lib/linter") } function getModuleFromCache(key) { if (!key.endsWith(needle)) return const module = require.cache[key] if (!module || !module.exports) return const Linter = module.exports if ( typeof Linter === "function" && typeof Linter.prototype.verify === "function" ) { return Linter } } function iterateESLintModules(fn) { if (!require.cache || Object.keys(require.cache).length === 0) { // Jest is replacing the node "require" function, and "require.cache" isn't available here. fn(getModuleFromRequire()) return } let found = false for (const key in require.cache) { const Linter = getModuleFromCache(key) if (Linter) { fn(Linter) found = true } } if (!found) { let eslintPath, eslintVersion try { eslintPath = require.resolve("eslint") } catch (e) { eslintPath = "(not found)" } try { eslintVersion = require("eslint/package.json").version } catch (e) { eslintVersion = "n/a" } const parentPaths = module => module ? [module.filename].concat(parentPaths(module.parent)) : [] throw new Error( `eslint-plugin-html error: It seems that eslint is not loaded. If you think this is a bug, please file a report at https://github.com/BenoitZugmeyer/eslint-plugin-html/issues In the report, please include *all* those informations: * ESLint version: ${eslintVersion} * ESLint path: ${eslintPath} * Plugin version: ${require("../package.json").version} * Plugin inclusion paths: ${parentPaths(module).join(", ")} * NodeJS version: ${process.version} * CLI arguments: ${JSON.stringify(process.argv)} * Content of your lock file (package-lock.json or yarn.lock) or the output of \`npm list\` * How did you run ESLint (via the command line? an editor plugin?) * The following stack trace: ${new Error().stack.slice(10)} ` ) } } function patch(Linter) { const verify = Linter.prototype.verify // ignore if verify function is already been patched sometime before if (Linter[LINTER_ISPATCHED_PROPERTY_NAME] === true) { return } Linter[LINTER_ISPATCHED_PROPERTY_NAME] = true Linter.prototype.verify = function( textOrSourceCode, config, filenameOrOptions, saveState ) { const localVerify = code => verify.call(this, code, config, filenameOrOptions, saveState) let messages const filename = typeof filenameOrOptions === "object" ? filenameOrOptions.filename : filenameOrOptions const extension = path.extname(filename || "") const pluginSettings = getSettings(config.settings || {}) const isHTML = pluginSettings.htmlExtensions.indexOf(extension) >= 0 const isXML = !isHTML && pluginSettings.xmlExtensions.indexOf(extension) >= 0 if (typeof textOrSourceCode === "string" && (isHTML || isXML)) { messages = [] const pushMessages = (localMessages, code) => { messages.push.apply( messages, remapMessages(localMessages, textOrSourceCode.startsWith(BOM), code) ) } const currentInfos = extract( textOrSourceCode, pluginSettings.indent, isXML, pluginSettings.isJavaScriptMIMEType ) if (pluginSettings.reportBadIndent) { currentInfos.badIndentationLines.forEach(line => { messages.push({ message: "Bad line indentation.", line, column: 1, ruleId: "(html plugin)", severity: pluginSettings.reportBadIndent, }) }) } if ( config.parserOptions && config.parserOptions.sourceType === "module" ) { for (const code of currentInfos.code) { pushMessages(localVerify(String(code)), code) } } else { verifyWithSharedScopes.call( this, localVerify, config, currentInfos, pushMessages ) } messages.sort((ma, mb) => { return ma.line - mb.line || ma.column - mb.column }) } else { messages = localVerify(textOrSourceCode) } return messages } } function verifyWithSharedScopes( localVerify, config, currentInfos, pushMessages ) { // First pass: collect needed globals and declared globals for each script tags. const firstPassValues = [] const originalRules = config.rules config.rules = { [GET_SCOPE_RULE_NAME]: "error" } for (const code of currentInfos.code) { this.rules.define(GET_SCOPE_RULE_NAME, context => { return { Program() { firstPassValues.push({ code, sourceCode: context.getSourceCode(), exportedGlobals: context .getScope() .through.map(node => node.identifier.name), declaredGlobals: context .getScope() .variables.map(variable => variable.name), }) }, } }) pushMessages(localVerify(String(code)), code) } config.rules = Object.assign( { [DECLARE_VARIABLES_RULE_NAME]: "error" }, originalRules ) // Second pass: declare variables for each script scope, then run eslint. for (let i = 0; i < firstPassValues.length; i += 1) { this.rules.define(DECLARE_VARIABLES_RULE_NAME, context => { return { Program() { const exportedGlobals = splatSet( firstPassValues .slice(i + 1) .map(nextValues => nextValues.exportedGlobals) ) for (const name of exportedGlobals) context.markVariableAsUsed(name) const declaredGlobals = splatSet( firstPassValues .slice(0, i) .map(previousValues => previousValues.declaredGlobals) ) const scope = context.getScope() scope.through = scope.through.filter(variable => { return !declaredGlobals.has(variable.identifier.name) }) }, } }) const values = firstPassValues[i] pushMessages(localVerify(values.sourceCode), values.code) } config.rules = originalRules } function remapMessages(messages, hasBOM, code) { const newMessages = [] const bomOffset = hasBOM ? -1 : 0 for (const message of messages) { const location = code.originalLocation({ line: message.line, // eslint-plugin-eslint-comments is raising message with column=0 to bypass ESLint ignore // comments. Since messages are already ignored at this time, just reset the column to a valid // number. See https://github.com/BenoitZugmeyer/eslint-plugin-html/issues/70 column: message.column || 1, }) // Ignore messages if they were in transformed code if (location) { Object.assign(message, location) message.source = code.getOriginalLine(location.line) // Map fix range if (message.fix && message.fix.range) { message.fix.range = [ code.originalIndex(message.fix.range[0]) + bomOffset, // The range end is exclusive, meaning it should replace all characters with indexes from // start to end - 1. We have to get the original index of the last targeted character. code.originalIndex(message.fix.range[1] - 1) + 1 + bomOffset, ] } // Map end location if (message.endLine && message.endColumn) { const endLocation = code.originalLocation({ line: message.endLine, column: message.endColumn, }) if (endLocation) { message.endLine = endLocation.line message.endColumn = endLocation.column } } newMessages.push(message) } } return newMessages }