UNPKG

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