eslint-plugin-html
Version:
A ESLint plugin to lint and fix inline scripts contained in HTML files.
771 lines (648 loc) • 20.9 kB
JavaScript
"use strict"
const path = require("path")
const CLIEngine = require("eslint").CLIEngine
const semver = require("semver")
const eslintVersion = require("eslint/package.json").version
const plugin = require("..")
function matchVersion(versionSpec) {
return semver.satisfies(eslintVersion, versionSpec, {
includePrerelease: true,
})
}
function ifVersion(versionSpec, fn, ...args) {
const execFn = matchVersion(versionSpec) ? fn : fn.skip
execFn(...args)
}
function execute(file, baseConfig) {
if (!baseConfig) baseConfig = {}
const cli = new CLIEngine({
extensions: ["html"],
baseConfig: {
settings: baseConfig.settings,
rules: Object.assign(
{
"no-console": 2,
},
baseConfig.rules
),
globals: baseConfig.globals,
env: baseConfig.env,
parserOptions: baseConfig.parserOptions,
},
ignore: false,
useEslintrc: false,
fix: baseConfig.fix,
reportUnusedDisableDirectives: baseConfig.reportUnusedDisableDirectives,
})
cli.addPlugin("html", plugin)
const results = cli.executeOnFiles([path.join(__dirname, "fixtures", file)])
.results[0]
return baseConfig.fix ? results : results && results.messages
}
it("should extract and remap messages", () => {
const messages = execute("simple.html")
expect(messages.length).toBe(5)
const hasEndPosition = messages[0].endLine !== undefined
expect(messages[0].message).toBe("Unexpected console statement.")
expect(messages[0].line).toBe(8)
expect(messages[0].column).toBe(7)
if (hasEndPosition) {
expect(messages[0].endLine).toBe(8)
expect(messages[0].endColumn).toBe(18)
}
expect(messages[1].message).toBe("Unexpected console statement.")
expect(messages[1].line).toBe(14)
expect(messages[1].column).toBe(7)
if (hasEndPosition) {
expect(messages[1].endLine).toBe(14)
expect(messages[1].endColumn).toBe(18)
}
expect(messages[2].message).toBe("Unexpected console statement.")
expect(messages[2].line).toBe(20)
expect(messages[2].column).toBe(3)
if (hasEndPosition) {
expect(messages[2].endLine).toBe(20)
expect(messages[2].endColumn).toBe(14)
}
expect(messages[3].message).toBe("Unexpected console statement.")
expect(messages[3].line).toBe(25)
expect(messages[3].column).toBe(11)
if (hasEndPosition) {
expect(messages[3].endLine).toBe(25)
expect(messages[3].endColumn).toBe(22)
}
expect(messages[4].message).toBe("Unexpected console statement.")
expect(messages[4].line).toBe(28)
expect(messages[4].column).toBe(13)
if (hasEndPosition) {
expect(messages[4].endLine).toBe(28)
expect(messages[4].endColumn).toBe(24)
}
})
it("should report correct line numbers with crlf newlines", () => {
const messages = execute("crlf-newlines.html")
expect(messages.length).toBe(1)
expect(messages[0].message).toBe("Unexpected console statement.")
expect(messages[0].line).toBe(8)
expect(messages[0].column).toBe(7)
})
describe("html/indent setting", () => {
it("should automatically compute indent when nothing is specified", () => {
const messages = execute("indent-setting.html", {
rules: {
indent: [2, 2],
},
})
expect(messages.length).toBe(0)
})
it("should work with a zero absolute indentation descriptor", () => {
const messages = execute("indent-setting.html", {
rules: {
indent: [2, 2],
},
settings: {
"html/indent": 0,
},
})
expect(messages.length).toBe(9)
// Only the first script is correctly indented (aligned on the first column)
expect(messages[0].message).toMatch(
/Expected indentation of 0 .* but found 2\./
)
expect(messages[0].line).toBe(16)
expect(messages[1].message).toMatch(
/Expected indentation of 2 .* but found 4\./
)
expect(messages[1].line).toBe(17)
expect(messages[2].message).toMatch(
/Expected indentation of 0 .* but found 2\./
)
expect(messages[2].line).toBe(18)
expect(messages[3].message).toMatch(
/Expected indentation of 0 .* but found 6\./
)
expect(messages[3].line).toBe(22)
expect(messages[4].message).toMatch(
/Expected indentation of 2 .* but found 8\./
)
expect(messages[4].line).toBe(23)
expect(messages[5].message).toMatch(
/Expected indentation of 0 .* but found 6\./
)
expect(messages[5].line).toBe(24)
expect(messages[6].message).toMatch(
/Expected indentation of 0 .* but found 10\./
)
expect(messages[6].line).toBe(28)
expect(messages[7].message).toMatch(
/Expected indentation of 2 .* but found 12\./
)
expect(messages[7].line).toBe(29)
expect(messages[8].message).toMatch(
/Expected indentation of 0 .* but found 10\./
)
expect(messages[8].line).toBe(30)
})
it("should work with a non-zero absolute indentation descriptor", () => {
const messages = execute("indent-setting.html", {
rules: {
indent: [2, 2],
},
settings: {
"html/indent": 2,
},
})
expect(messages.length).toBe(7)
// The first script is incorrect since the second line gets dedented
expect(messages[0].message).toMatch(
/Expected indentation of 2 .* but found 0\./
)
expect(messages[0].line).toBe(11)
// The second script is correct.
expect(messages[1].message).toMatch(
/Expected indentation of 0 .* but found 6\./
)
expect(messages[1].line).toBe(22)
expect(messages[2].message).toMatch(
/Expected indentation of .* but found 6\./
)
expect(messages[2].line).toBe(23)
expect(messages[3].message).toMatch(
/Expected indentation of .* but found 4\./
)
expect(messages[3].line).toBe(24)
expect(messages[4].message).toMatch(
/Expected indentation of 0 .* but found 10\./
)
expect(messages[4].line).toBe(28)
expect(messages[5].message).toMatch(
/Expected indentation of .* but found 10\./
)
expect(messages[5].line).toBe(29)
expect(messages[6].message).toMatch(
/Expected indentation of .* but found 8\./
)
expect(messages[6].line).toBe(30)
})
it("should work with relative indentation descriptor", () => {
const messages = execute("indent-setting.html", {
rules: {
indent: [2, 2],
},
settings: {
"html/indent": "+2",
},
})
expect(messages.length).toBe(6)
// The first script is correct since it can't be dedented, but follows the indent
// rule anyway.
expect(messages[0].message).toMatch(
/Expected indentation of 0 .* but found 2\./
)
expect(messages[0].line).toBe(16)
expect(messages[1].message).toMatch(
/Expected indentation of 2 .* but found 4\./
)
expect(messages[1].line).toBe(17)
expect(messages[2].message).toMatch(
/Expected indentation of 0 .* but found 2\./
)
expect(messages[2].line).toBe(18)
// The third script is correct.
expect(messages[3].message).toMatch(
/Expected indentation of 0 .* but found 10\./
)
expect(messages[3].line).toBe(28)
expect(messages[4].message).toMatch(
/Expected indentation of 2 .* but found 4\./
)
expect(messages[4].line).toBe(29)
expect(messages[5].message).toMatch(
/Expected indentation of 0 .* but found 2\./
)
expect(messages[5].line).toBe(30)
})
it("should report messages at the beginning of the file", () => {
const messages = execute("error-at-the-beginning.html", {
rules: {
"max-lines": [2, { max: 1 }],
"max-len": [2, { code: 35 }],
"no-console": 0,
},
})
expect(messages.length).toBe(2)
expect(messages[0].message).toBe(
matchVersion(">= 6")
? "This line has a length of 70. Maximum allowed is 35."
: "Line 1 exceeds the maximum line length of 35."
)
expect(messages[0].line).toBe(1)
expect(messages[0].column).toBe(9)
expect(messages[1].message).toBe(
matchVersion(">= 6")
? "File has too many lines (7). Maximum allowed is 1."
: "File must be at most 1 lines long. It's 7 lines long."
)
expect(messages[1].line).toBe(1)
expect(messages[1].column).toBe(9)
})
})
describe("html/report-bad-indent setting", () => {
it("should report under-indented code with auto indent setting", () => {
const messages = execute("report-bad-indent-setting.html", {
settings: {
"html/report-bad-indent": true,
},
})
expect(messages.length).toBe(1)
expect(messages[0].message).toBe("Bad line indentation.")
expect(messages[0].line).toBe(10)
expect(messages[0].column).toBe(1)
})
it("should report under-indented code with provided indent setting", () => {
const messages = execute("report-bad-indent-setting.html", {
settings: {
"html/report-bad-indent": true,
"html/indent": "+4",
},
})
expect(messages.length).toBe(3)
expect(messages[0].message).toBe("Bad line indentation.")
expect(messages[0].line).toBe(9)
expect(messages[0].column).toBe(1)
expect(messages[1].message).toBe("Bad line indentation.")
expect(messages[1].line).toBe(10)
expect(messages[1].column).toBe(1)
expect(messages[2].message).toBe("Bad line indentation.")
expect(messages[2].line).toBe(11)
expect(messages[2].column).toBe(1)
})
})
describe("xml support", () => {
it("consider .html files as HTML", () => {
const messages = execute("cdata.html")
expect(messages.length).toBe(1)
expect(messages[0].message).toBe("Parsing error: Unexpected token <")
expect(messages[0].fatal).toBe(true)
expect(messages[0].line).toBe(10)
expect(messages[0].column).toBe(7)
})
it("can be forced to consider .html files as XML", () => {
const messages = execute("cdata.html", {
settings: {
"html/xml-extensions": [".html"],
},
})
expect(messages.length).toBe(1)
expect(messages[0].message).toBe("Unexpected console statement.")
expect(messages[0].line).toBe(11)
expect(messages[0].column).toBe(9)
})
it("consider .xhtml files as XML", () => {
const messages = execute("cdata.xhtml")
expect(messages.length).toBe(1)
expect(messages[0].message).toBe("Unexpected console statement.")
expect(messages[0].line).toBe(13)
expect(messages[0].column).toBe(9)
})
it("can be forced to consider .xhtml files as HTML", () => {
const messages = execute("cdata.xhtml", {
settings: {
"html/html-extensions": [".xhtml"],
},
})
expect(messages.length).toBe(1)
expect(messages[0].message).toBe("Parsing error: Unexpected token <")
expect(messages[0].fatal).toBe(true)
expect(messages[0].line).toBe(12)
expect(messages[0].column).toBe(7)
})
it("removes white space at the end of scripts ending with CDATA", () => {
const messages = execute("cdata.xhtml", {
rules: {
"no-console": "off",
"no-trailing-spaces": "error",
"eol-last": "error",
},
})
expect(messages.length).toBe(0)
})
it("should support self closing script tags", () => {
let messages
expect(() => {
messages = execute("self-closing-tags.xhtml")
}).not.toThrow()
expect(messages.length).toBe(0)
})
})
describe("lines-around-comment and multiple scripts", () => {
it("should not warn with lines-around-comment if multiple scripts", () => {
const messages = execute("simple.html", {
rules: {
"lines-around-comment": ["error", { beforeLineComment: true }],
},
})
expect(messages.length).toBe(5)
})
})
describe("fix", () => {
it("should remap fix ranges", () => {
const messages = execute("fix.html", {
rules: {
"no-extra-semi": ["error"],
},
})
expect(messages[0].fix.range).toEqual([53, 55])
})
it("should fix errors", () => {
const result = execute("fix.html", {
rules: {
"no-extra-semi": ["error"],
},
fix: true,
})
expect(result.output).toBe(`<!DOCTYPE html>
<html lang="en">
<script>
foo();
</script>
</html>
`)
expect(result.messages.length).toBe(0)
})
it("should fix errors in files with BOM", () => {
const result = execute("fix-bom.html", {
rules: {
"no-extra-semi": ["error"],
},
fix: true,
})
expect(result.output).toBe(`\uFEFF<!DOCTYPE html>
<html lang="en">
<script>
foo();
</script>
</html>
`)
expect(result.messages.length).toBe(0)
})
describe("eol-last rule", () => {
it("should work with eol-last always", () => {
const result = execute("fix.html", {
rules: {
"eol-last": ["error"],
"no-extra-semi": ["error"],
},
fix: true,
})
expect(result.output).toBe(`<!DOCTYPE html>
<html lang="en">
<script>
foo();
</script>
</html>
`)
expect(result.messages.length).toBe(0)
})
it("should work with eol-last never", () => {
const result = execute("fix.html", {
rules: {
"eol-last": ["error", "never"],
},
fix: true,
})
expect(result.output).toBe(`<!DOCTYPE html>
<html lang="en">
<script>
foo();; </script>
</html>
`)
expect(result.messages.length).toBe(0)
})
})
})
ifVersion(">= 4.8.0", describe, "reportUnusedDisableDirectives", () => {
it("reports unused disabled directives", () => {
const messages = execute("inline-disabled-rule.html", {
reportUnusedDisableDirectives: true,
})
expect(messages.length).toBe(1)
expect(messages[0].line).toBe(2)
expect(messages[0].column).toBe(3)
expect(messages[0].message).toBe(
"Unused eslint-disable directive (no problems were reported from 'no-eval')."
)
})
it("doesn't report used disabled directives", () => {
const messages = execute("inline-disabled-rule.html", {
reportUnusedDisableDirectives: true,
rules: {
"no-eval": 2,
},
})
expect(messages.length).toBe(0)
})
})
describe("html/javascript-mime-types", () => {
it("ignores unknown mime types by default", () => {
const messages = execute("javascript-mime-types.html")
expect(messages.length).toBe(3)
expect(messages[0].ruleId).toBe("no-console")
expect(messages[0].line).toBe(8)
expect(messages[1].ruleId).toBe("no-console")
expect(messages[1].line).toBe(12)
expect(messages[2].ruleId).toBe("no-console")
expect(messages[2].line).toBe(16)
})
it("specifies a list of valid mime types", () => {
const messages = execute("javascript-mime-types.html", {
settings: {
"html/javascript-mime-types": ["text/foo"],
},
})
expect(messages.length).toBe(2)
expect(messages[0].ruleId).toBe("no-console")
expect(messages[0].line).toBe(8)
expect(messages[1].ruleId).toBe("no-console")
expect(messages[1].line).toBe(20)
})
it("specifies a regexp of valid mime types", () => {
const messages = execute("javascript-mime-types.html", {
settings: {
"html/javascript-mime-types": "/^(application|text)/foo$/",
},
})
expect(messages.length).toBe(3)
expect(messages[0].ruleId).toBe("no-console")
expect(messages[0].line).toBe(8)
expect(messages[1].ruleId).toBe("no-console")
expect(messages[1].line).toBe(20)
expect(messages[2].ruleId).toBe("no-console")
expect(messages[2].line).toBe(24)
})
})
it("should report correct eol-last message position", () => {
const messages = execute("eol-last.html", {
rules: {
"eol-last": "error",
},
})
expect(messages.length).toBe(1)
expect(messages[0].ruleId).toBe("eol-last")
expect(messages[0].line).toBe(6)
expect(messages[0].column).toBe(42)
})
describe("scope sharing", () => {
it("should export global variables between script scopes", () => {
const messages = execute("scope-sharing.html", {
rules: {
"no-console": "off",
"no-undef": "error",
},
globals: {
console: false,
},
env: { es6: true },
})
expect(messages.length).toBe(4)
expect(messages[0].line).toBe(13)
expect(messages[0].message).toBe(
"'varNotYetGloballyDeclared' is not defined."
)
expect(messages[1].line).toBe(14)
expect(messages[1].message).toBe(
"'letNotYetGloballyDeclared' is not defined."
)
expect(messages[2].line).toBe(15)
expect(messages[2].message).toBe(
"'functionNotYetGloballyDeclared' is not defined."
)
expect(messages[3].line).toBe(16)
expect(messages[3].message).toBe(
"'ClassNotYetGloballyDeclared' is not defined."
)
})
it("should mark variable as used when the variable is used in another tag", () => {
const messages = execute("scope-sharing.html", {
rules: {
"no-console": "off",
"no-unused-vars": "error",
},
globals: {
console: false,
},
env: { es6: true },
})
expect(messages.length).toBe(4)
expect(messages[0].line).toBe(20)
expect(messages[0].message).toBe(
"'varNotYetGloballyDeclared' is assigned a value but never used."
)
expect(messages[1].line).toBe(21)
expect(messages[1].message).toBe(
"'letNotYetGloballyDeclared' is assigned a value but never used."
)
expect(messages[2].line).toBe(22)
expect(messages[2].message).toBe(
"'functionNotYetGloballyDeclared' is defined but never used."
)
expect(messages[3].line).toBe(23)
expect(messages[3].message).toBe(
"'ClassNotYetGloballyDeclared' is defined but never used."
)
})
it("should not be influenced by the ECMA feature 'globalReturn'", () => {
const messages = execute("scope-sharing.html", {
rules: {
"no-console": "off",
"no-undef": "error",
"no-unused-vars": "error",
},
globals: {
console: false,
},
env: { es6: true },
parserOptions: {
ecmaFeatures: {
globalReturn: true,
},
},
})
expect(messages.length).toBe(8)
})
it("should not share the global scope if sourceType is 'module'", () => {
const messages = execute("scope-sharing.html", {
rules: {
"no-console": "off",
"no-undef": "error",
"no-unused-vars": "error",
},
globals: {
console: false,
},
env: { es6: true },
parserOptions: {
sourceType: "module",
},
})
expect(messages.length).toBe(16)
expect(messages[0].line).toBe(8)
expect(messages[0].message).toBe(
"'varGloballyDeclared' is assigned a value but never used."
)
expect(messages[1].line).toBe(9)
expect(messages[1].message).toBe(
"'letGloballyDeclared' is assigned a value but never used."
)
expect(messages[2].line).toBe(10)
expect(messages[2].message).toBe(
"'functionGloballyDeclared' is defined but never used."
)
expect(messages[3].line).toBe(11)
expect(messages[3].message).toBe(
"'ClassGloballyDeclared' is defined but never used."
)
expect(messages[4].line).toBe(13)
expect(messages[4].message).toBe(
"'varNotYetGloballyDeclared' is not defined."
)
expect(messages[5].line).toBe(14)
expect(messages[5].message).toBe(
"'letNotYetGloballyDeclared' is not defined."
)
expect(messages[6].line).toBe(15)
expect(messages[6].message).toBe(
"'functionNotYetGloballyDeclared' is not defined."
)
expect(messages[7].line).toBe(16)
expect(messages[7].message).toBe(
"'ClassNotYetGloballyDeclared' is not defined."
)
expect(messages[8].line).toBe(20)
expect(messages[8].message).toBe(
"'varNotYetGloballyDeclared' is assigned a value but never used."
)
expect(messages[9].line).toBe(21)
expect(messages[9].message).toBe(
"'letNotYetGloballyDeclared' is assigned a value but never used."
)
expect(messages[10].line).toBe(22)
expect(messages[10].message).toBe(
"'functionNotYetGloballyDeclared' is defined but never used."
)
expect(messages[11].line).toBe(23)
expect(messages[11].message).toBe(
"'ClassNotYetGloballyDeclared' is defined but never used."
)
expect(messages[12].line).toBe(25)
expect(messages[12].message).toBe("'varGloballyDeclared' is not defined.")
expect(messages[13].line).toBe(26)
expect(messages[13].message).toBe("'letGloballyDeclared' is not defined.")
expect(messages[14].line).toBe(27)
expect(messages[14].message).toBe(
"'functionGloballyDeclared' is not defined."
)
expect(messages[15].line).toBe(28)
expect(messages[15].message).toBe("'ClassGloballyDeclared' is not defined.")
})
})