Skip to content

Commit

Permalink
feat: support eslint 9
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Feb 9, 2024
1 parent 17f8838 commit d7cde04
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 39 deletions.
96 changes: 72 additions & 24 deletions src/__tests__/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,69 @@ function ifVersion(versionSpec, fn, ...args) {
async function execute(file, options = {}) {
const files = [path.join(__dirname, "fixtures", file)]

const eslintOptions = {
extensions: ["html"],
baseConfig: {
settings: options.settings,
rules: Object.assign(
{
"no-console": 2,
let eslintOptions
if (matchVersion(">= 9")) {
eslintOptions = {
plugins: {
html: require(".."),
},
baseConfig: {
files: ["**/*.html", "**/*.xhtml"],
settings: options.settings || {},
rules: Object.assign(
{
"no-console": 2,
},
options.rules
),
languageOptions: {
globals: options.globals || {},
sourceType: "script",
parserOptions: options.parserOptions || {},
...("parser" in options ? { parser: options.parser } : {}),
},
options.rules
),
globals: options.globals,
env: options.env,
parserOptions: options.parserOptions,
parser: options.parser,
},
ignore: false,
useEslintrc: false,
fix: options.fix,
reportUnusedDisableDirectives:
options.reportUnusedDisableDirectives || null,
linterOptions: {
...("reportUnusedDisableDirective" in options
? {
reportUnusedDisableDirectives:
options.reportUnusedDisableDirectives,
}
: {}),
},
plugins: options.plugins || {},
},
ignore: false,
ignorePatterns: [],
overrideConfigFile: true,
fix: options.fix,
}
} else {
eslintOptions = {
extensions: ["html"],
baseConfig: {
settings: options.settings,
rules: Object.assign(
{
"no-console": 2,
},
options.rules
),
globals: options.globals,
env: options.env,
parserOptions: options.parserOptions,
parser: options.parser,
plugins: options.plugins,
},
ignore: false,
useEslintrc: false,
fix: options.fix,
reportUnusedDisableDirectives:
options.reportUnusedDisableDirectives || null,
}
}

let results
if (eslint.ESLint) {
eslintOptions.baseConfig.plugins = options.plugins
const instance = new eslint.ESLint(eslintOptions)
results = (await instance.lintFiles(files))[0]
} else if (eslint.CLIEngine) {
Expand Down Expand Up @@ -794,10 +832,21 @@ describe("scope sharing", () => {

// For some reason @html-eslint is not compatible with ESLint < 5
ifVersion(">= 5", describe, "compatibility with external HTML plugins", () => {
const BASE_HTML_ESLINT_CONFIG = matchVersion(">= 9")
? {
plugins: {
"@html-eslint": require("@html-eslint/eslint-plugin"),
},
parser: require("@html-eslint/parser"),
}
: {
plugins: ["@html-eslint/eslint-plugin"],
parser: "@html-eslint/parser",
}

it("check", async () => {
const messages = await execute("other-html-plugins-compatibility.html", {
plugins: ["@html-eslint/eslint-plugin"],
parser: "@html-eslint/parser",
...BASE_HTML_ESLINT_CONFIG,
rules: {
"@html-eslint/require-img-alt": ["error"],
},
Expand Down Expand Up @@ -843,8 +892,7 @@ ifVersion(">= 5", describe, "compatibility with external HTML plugins", () => {

it("fix", async () => {
const result = await execute("other-html-plugins-compatibility.html", {
plugins: ["@html-eslint/eslint-plugin"],
parser: "@html-eslint/parser",
...BASE_HTML_ESLINT_CONFIG,
rules: {
"@html-eslint/quotes": ["error", "single"],
quotes: ["error", "single"],
Expand Down
18 changes: 13 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

const path = require("path")
const { createVerifyPatch } = require("./verifyPatch")
const {
createVerifyWithFlatConfigPatch,
} = require("./verifyWithFlatConfigPatch")

const LINTER_ISPATCHED_PROPERTY_NAME =
"__eslint-plugin-html-verify-function-is-patched"
Expand Down Expand Up @@ -105,15 +108,20 @@ In the report, please include *all* those informations:
}

function patch(Linter) {
const verifyMethodName = Linter.prototype._verifyWithoutProcessors
? "_verifyWithoutProcessors" // ESLint 6+
: "verify" // ESLint 5-
const verify = Linter.prototype[verifyMethodName]

// ignore if verify function is already been patched sometime before
if (Linter[LINTER_ISPATCHED_PROPERTY_NAME] === true) {
return
}
Linter[LINTER_ISPATCHED_PROPERTY_NAME] = true

const verifyMethodName = Linter.prototype._verifyWithoutProcessors
? "_verifyWithoutProcessors" // ESLint 6+
: "verify" // ESLint 5-
const verify = Linter.prototype[verifyMethodName]
Linter.prototype[verifyMethodName] = createVerifyPatch(verify)

const verifyWithFlatConfig =
Linter.prototype._verifyWithFlatConfigArrayAndWithoutProcessors
Linter.prototype._verifyWithFlatConfigArrayAndWithoutProcessors =
createVerifyWithFlatConfigPatch(verifyWithFlatConfig)
}
4 changes: 2 additions & 2 deletions src/verifyPatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ function createVerifyPatch(verify) {
this.defineRule(PREPARE_RULE_NAME, (context) => {
sourceCodes.set(codePart, context.getSourceCode())
return {
Program() {
Program(program) {
if (prepare) {
prepare(context)
prepare(context, program)
}
},
}
Expand Down
162 changes: 162 additions & 0 deletions src/verifyWithFlatConfigPatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const getSettings = require("./settings").getSettings
const getFileMode = require("./getFileMode")
const extract = require("./extract")
const { verifyWithSharedScopes } = require("./verifyWithSharedScopes")
const { remapMessages } = require("./remapMessages")

const PREPARE_RULE_NAME = "__eslint-plugin-html-prepare"
const PREPARE_PLUGIN_NAME = "__eslint-plugin-html-prepare"

module.exports = { createVerifyWithFlatConfigPatch }

function createVerifyWithFlatConfigPatch(verifyWithFlatConfig) {
return function (textOrSourceCode, providedConfig, providedOptions) {
const callOriginalVerify = () =>
verifyWithFlatConfig.call(
this,
textOrSourceCode,
providedConfig,
providedOptions
)

const pluginSettings = getSettings(providedConfig.settings || {})
const mode = getFileMode(pluginSettings, providedOptions.filename)

let messages
;[messages, providedConfig] = verifyExternalHtmlPlugin(
providedConfig,
callOriginalVerify
)

const extractResult = extract(
textOrSourceCode,
pluginSettings.indent,
mode === "xml",
pluginSettings.javaScriptTagNames,
pluginSettings.isJavaScriptMIMEType
)

if (pluginSettings.reportBadIndent) {
messages.push(
...extractResult.badIndentationLines.map((line) => ({
message: "Bad line indentation.",
line,
column: 1,
ruleId: "(html plugin)",
severity: pluginSettings.reportBadIndent,
}))
)
}

// Save code parts parsed source code so we don't have to parse it twice
const sourceCodes = new WeakMap()
const verifyCodePart = (codePart, { prepare, ignoreRules } = {}) => {
providedConfig.plugins[PREPARE_PLUGIN_NAME] = {
rules: {
[PREPARE_RULE_NAME]: {
create(context) {
sourceCodes.set(codePart, context.getSourceCode())
return {
Program(program) {
if (prepare) {
prepare(context, program)
}
},
}
},
},
},
}

const localMessages = verifyWithFlatConfig.call(
this,
sourceCodes.get(codePart) || String(codePart),
{
...providedConfig,
rules: Object.assign(
{ [`${PREPARE_PLUGIN_NAME}/${PREPARE_RULE_NAME}`]: "error" },
!ignoreRules && providedConfig.rules
),
},
ignoreRules
? {
...providedOptions,
reportUnusedDisableDirectives: false,
}
: providedOptions
)

messages.push(
...remapMessages(localMessages, extractResult.hasBOM, codePart)
)
}

const parserOptions = providedConfig.languageOptions.parserOptions || {}
if (parserOptions.sourceType === "module") {
for (const codePart of extractResult.code) {
verifyCodePart(codePart)
}
} else {
verifyWithSharedScopes(extractResult.code, verifyCodePart, parserOptions)
}

messages.sort((ma, mb) => ma.line - mb.line || ma.column - mb.column)

return messages
}
}

const externalHtmlPlugins = [
{ parser: "@html-eslint/parser", plugin: "@html-eslint/eslint-plugin" },
]

function tryRequire(name) {
try {
return require(name)
} catch (e) {
return undefined
}
}

function findExternalHtmlPluginName(config) {
if (!config.languageOptions || !config.languageOptions.parser) {
return
}
for (const { parser, plugin } of externalHtmlPlugins) {
let parserModule = tryRequire(parser)
if (config.languageOptions.parser === parserModule) {
const pluginModule = tryRequire(plugin)
for (const [name, plugin] of Object.entries(config.plugins)) {
if (plugin === pluginModule) {
return name
}
}
}
}
}

function verifyExternalHtmlPlugin(config, callOriginalVerify) {
const htmlPluginName = findExternalHtmlPluginName(config)
if (!htmlPluginName) {
return [[], config]
}

const rules = {}
for (const name in config.rules) {
if (!name.startsWith(htmlPluginName + "/")) {
rules[name] = config.rules[name]
}
}

return [
callOriginalVerify(),
{
...config,
languageOptions: {
...config.languageOptions,
parser: require("espree"),
},
rules,
},
]
}
30 changes: 25 additions & 5 deletions src/verifyWithSharedScopes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {

for (const codePart of codeParts) {
verifyCodePart(codePart, {
prepare(context) {
const globalScope = context.getScope()
prepare(context, program) {
const globalScope = getGlobalScope(context, program)
// See https://github.com/eslint/eslint/blob/4b267a5c8a42477bb2384f33b20083ff17ad578c/lib/rules/no-redeclare.js#L67-L78
let scopeForDeclaredGlobals
if (
Expand Down Expand Up @@ -38,24 +38,44 @@ function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {
// Second pass: declare variables for each script scope, then run eslint.
for (let i = 0; i < firstPassValues.length; i += 1) {
verifyCodePart(firstPassValues[i].codePart, {
prepare(context) {
prepare(context, program) {
const exportedGlobals = splatSet(
firstPassValues
.slice(i + 1)
.map((nextValues) => nextValues.exportedGlobals)
)
for (const name of exportedGlobals) context.markVariableAsUsed(name)
for (const name of exportedGlobals)
markGlobalVariableAsUsed(context, program, name)

const declaredGlobals = splatSet(
firstPassValues
.slice(0, i)
.map((previousValues) => previousValues.declaredGlobals)
)
const scope = context.getScope()
const scope = getGlobalScope(context, program)
scope.through = scope.through.filter((variable) => {
return !declaredGlobals.has(variable.identifier.name)
})
},
})
}
}

function markGlobalVariableAsUsed(context, program, name) {
const sourceCode = context.getSourceCode()

if (sourceCode.markVariableAsUsed) {
sourceCode.markVariableAsUsed(name, program)
} else {
context.markVariableAsUsed(name)
}
}

function getGlobalScope(context, program) {
const sourceCode = context.getSourceCode()
if (sourceCode.getScope) {
// eslint 9+
return sourceCode.getScope(program)
}
return context.getScope()
}
Loading

0 comments on commit d7cde04

Please sign in to comment.