UNPKG

9.97 kBJavaScriptView Raw
1"use strict"
2
3const path = require("path")
4const extract = require("./extract")
5const utils = require("./utils")
6const splatSet = utils.splatSet
7const getSettings = require("./settings").getSettings
8
9const PREPARE_RULE_NAME = "__eslint-plugin-html-prepare"
10const LINTER_ISPATCHED_PROPERTY_NAME =
11 "__eslint-plugin-html-verify-function-is-patched"
12
13// Disclaimer:
14//
15// This is not a long term viable solution. ESLint needs to improve its processor API to
16// provide access to the configuration before actually preprocess files, but it's not
17// planed yet. This solution is quite ugly but shouldn't alter eslint process.
18//
19// Related github issues:
20// https://github.com/eslint/eslint/issues/3422
21// https://github.com/eslint/eslint/issues/4153
22
23const needles = [
24 path.join("lib", "linter", "linter.js"), // ESLint 6+
25 path.join("lib", "linter.js"), // ESLint 5-
26]
27
28iterateESLintModules(patch)
29
30function getLinterFromModule(moduleExports) {
31 return moduleExports.Linter
32 ? moduleExports.Linter // ESLint 6+
33 : moduleExports // ESLint 5-
34}
35
36function getModuleFromRequire() {
37 return getLinterFromModule(require("eslint/lib/linter"))
38}
39
40function getModuleFromCache(key) {
41 if (!needles.some((needle) => key.endsWith(needle))) return
42
43 const module = require.cache[key]
44 if (!module || !module.exports) return
45
46 const Linter = getLinterFromModule(module.exports)
47 if (
48 typeof Linter === "function" &&
49 typeof Linter.prototype.verify === "function"
50 ) {
51 return Linter
52 }
53}
54
55function iterateESLintModules(fn) {
56 if (!require.cache || Object.keys(require.cache).length === 0) {
57 // Jest is replacing the node "require" function, and "require.cache" isn't available here.
58 fn(getModuleFromRequire())
59 return
60 }
61
62 let found = false
63
64 for (const key in require.cache) {
65 const Linter = getModuleFromCache(key)
66 if (Linter) {
67 fn(Linter)
68 found = true
69 }
70 }
71
72 if (!found) {
73 let eslintPath, eslintVersion
74 try {
75 eslintPath = require.resolve("eslint")
76 } catch (e) {
77 eslintPath = "(not found)"
78 }
79 try {
80 eslintVersion = require("eslint/package.json").version
81 } catch (e) {
82 eslintVersion = "n/a"
83 }
84
85 const parentPaths = (module) =>
86 module ? [module.filename].concat(parentPaths(module.parent)) : []
87
88 throw new Error(
89 `eslint-plugin-html error: It seems that eslint is not loaded.
90If you think this is a bug, please file a report at https://github.com/BenoitZugmeyer/eslint-plugin-html/issues
91
92In the report, please include *all* those informations:
93
94* ESLint version: ${eslintVersion}
95* ESLint path: ${eslintPath}
96* Plugin version: ${require("../package.json").version}
97* Plugin inclusion paths: ${parentPaths(module).join(", ")}
98* NodeJS version: ${process.version}
99* CLI arguments: ${JSON.stringify(process.argv)}
100* Content of your lock file (package-lock.json or yarn.lock) or the output of \`npm list\`
101* How did you run ESLint (via the command line? an editor plugin?)
102* The following stack trace:
103 ${new Error().stack.slice(10)}
104
105
106 `
107 )
108 }
109}
110
111function getMode(pluginSettings, filenameOrOptions) {
112 const filename =
113 typeof filenameOrOptions === "object"
114 ? filenameOrOptions.filename
115 : filenameOrOptions
116 const extension = path.extname(filename || "")
117
118 if (pluginSettings.htmlExtensions.indexOf(extension) >= 0) {
119 return "html"
120 }
121 if (pluginSettings.xmlExtensions.indexOf(extension) >= 0) {
122 return "xml"
123 }
124}
125
126function patch(Linter) {
127 const verifyMethodName = Linter.prototype._verifyWithoutProcessors
128 ? "_verifyWithoutProcessors" // ESLint 6+
129 : "verify" // ESLint 5-
130 const verify = Linter.prototype[verifyMethodName]
131
132 // ignore if verify function is already been patched sometime before
133 if (Linter[LINTER_ISPATCHED_PROPERTY_NAME] === true) {
134 return
135 }
136 Linter[LINTER_ISPATCHED_PROPERTY_NAME] = true
137 Linter.prototype[verifyMethodName] = function (
138 textOrSourceCode,
139 config,
140 filenameOrOptions,
141 saveState
142 ) {
143 if (typeof config.extractConfig === "function") {
144 return verify.call(this, textOrSourceCode, config, filenameOrOptions)
145 }
146
147 const pluginSettings = getSettings(config.settings || {})
148 const mode = getMode(pluginSettings, filenameOrOptions)
149
150 if (!mode || typeof textOrSourceCode !== "string") {
151 return verify.call(
152 this,
153 textOrSourceCode,
154 config,
155 filenameOrOptions,
156 saveState
157 )
158 }
159 const extractResult = extract(
160 textOrSourceCode,
161 pluginSettings.indent,
162 mode === "xml",
163 pluginSettings.isJavaScriptMIMEType
164 )
165
166 const messages = []
167
168 if (pluginSettings.reportBadIndent) {
169 messages.push(
170 ...extractResult.badIndentationLines.map((line) => ({
171 message: "Bad line indentation.",
172 line,
173 column: 1,
174 ruleId: "(html plugin)",
175 severity: pluginSettings.reportBadIndent,
176 }))
177 )
178 }
179
180 // Save code parts parsed source code so we don't have to parse it twice
181 const sourceCodes = new WeakMap()
182 const verifyCodePart = (codePart, { prepare, ignoreRules } = {}) => {
183 this.defineRule(PREPARE_RULE_NAME, (context) => {
184 sourceCodes.set(codePart, context.getSourceCode())
185 return {
186 Program() {
187 if (prepare) {
188 prepare(context)
189 }
190 },
191 }
192 })
193
194 const localMessages = verify.call(
195 this,
196 sourceCodes.get(codePart) || String(codePart),
197 Object.assign({}, config, {
198 rules: Object.assign(
199 { [PREPARE_RULE_NAME]: "error" },
200 !ignoreRules && config.rules
201 ),
202 }),
203 ignoreRules && typeof filenameOrOptions === "object"
204 ? Object.assign({}, filenameOrOptions, {
205 reportUnusedDisableDirectives: false,
206 })
207 : filenameOrOptions,
208 saveState
209 )
210
211 messages.push(
212 ...remapMessages(localMessages, extractResult.hasBOM, codePart)
213 )
214 }
215
216 const parserOptions = config.parserOptions || {}
217 if (parserOptions.sourceType === "module") {
218 for (const codePart of extractResult.code) {
219 verifyCodePart(codePart)
220 }
221 } else {
222 verifyWithSharedScopes(extractResult.code, verifyCodePart, parserOptions)
223 }
224
225 messages.sort((ma, mb) => ma.line - mb.line || ma.column - mb.column)
226
227 return messages
228 }
229}
230
231function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {
232 // First pass: collect needed globals and declared globals for each script tags.
233 const firstPassValues = []
234
235 for (const codePart of codeParts) {
236 verifyCodePart(codePart, {
237 prepare(context) {
238 const globalScope = context.getScope()
239 // See https://github.com/eslint/eslint/blob/4b267a5c8a42477bb2384f33b20083ff17ad578c/lib/rules/no-redeclare.js#L67-L78
240 let scopeForDeclaredGlobals
241 if (
242 parserOptions.ecmaFeatures &&
243 parserOptions.ecmaFeatures.globalReturn
244 ) {
245 scopeForDeclaredGlobals = globalScope.childScopes[0]
246 } else {
247 scopeForDeclaredGlobals = globalScope
248 }
249
250 firstPassValues.push({
251 codePart,
252 exportedGlobals: globalScope.through.map(
253 (node) => node.identifier.name
254 ),
255 declaredGlobals: scopeForDeclaredGlobals.variables.map(
256 (variable) => variable.name
257 ),
258 })
259 },
260 ignoreRules: true,
261 })
262 }
263
264 // Second pass: declare variables for each script scope, then run eslint.
265 for (let i = 0; i < firstPassValues.length; i += 1) {
266 verifyCodePart(firstPassValues[i].codePart, {
267 prepare(context) {
268 const exportedGlobals = splatSet(
269 firstPassValues
270 .slice(i + 1)
271 .map((nextValues) => nextValues.exportedGlobals)
272 )
273 for (const name of exportedGlobals) context.markVariableAsUsed(name)
274
275 const declaredGlobals = splatSet(
276 firstPassValues
277 .slice(0, i)
278 .map((previousValues) => previousValues.declaredGlobals)
279 )
280 const scope = context.getScope()
281 scope.through = scope.through.filter((variable) => {
282 return !declaredGlobals.has(variable.identifier.name)
283 })
284 },
285 })
286 }
287}
288
289function remapMessages(messages, hasBOM, codePart) {
290 const newMessages = []
291 const bomOffset = hasBOM ? -1 : 0
292
293 for (const message of messages) {
294 const location = codePart.originalLocation({
295 line: message.line,
296 // eslint-plugin-eslint-comments is raising message with column=0 to bypass ESLint ignore
297 // comments. Since messages are already ignored at this time, just reset the column to a valid
298 // number. See https://github.com/BenoitZugmeyer/eslint-plugin-html/issues/70
299 column: message.column || 1,
300 })
301
302 // Ignore messages if they were in transformed code
303 if (location) {
304 Object.assign(message, location)
305 message.source = codePart.getOriginalLine(location.line)
306
307 // Map fix range
308 if (message.fix && message.fix.range) {
309 message.fix.range = [
310 codePart.originalIndex(message.fix.range[0]) + bomOffset,
311 // The range end is exclusive, meaning it should replace all characters with indexes from
312 // start to end - 1. We have to get the original index of the last targeted character.
313 codePart.originalIndex(message.fix.range[1] - 1) + 1 + bomOffset,
314 ]
315 }
316
317 // Map end location
318 if (message.endLine && message.endColumn) {
319 const endLocation = codePart.originalLocation({
320 line: message.endLine,
321 column: message.endColumn,
322 })
323 if (endLocation) {
324 message.endLine = endLocation.line
325 message.endColumn = endLocation.column
326 }
327 }
328
329 newMessages.push(message)
330 }
331 }
332
333 return newMessages
334}