1 | "use strict"
|
2 |
|
3 | const path = require("path")
|
4 | const extract = require("./extract")
|
5 | const utils = require("./utils")
|
6 | const splatSet = utils.splatSet
|
7 | const getSettings = require("./settings").getSettings
|
8 |
|
9 | const PREPARE_RULE_NAME = "__eslint-plugin-html-prepare"
|
10 | const LINTER_ISPATCHED_PROPERTY_NAME =
|
11 | "__eslint-plugin-html-verify-function-is-patched"
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | const needles = [
|
24 | path.join("lib", "linter", "linter.js"),
|
25 | path.join("lib", "linter.js"),
|
26 | ]
|
27 |
|
28 | iterateESLintModules(patch)
|
29 |
|
30 | function getLinterFromModule(moduleExports) {
|
31 | return moduleExports.Linter
|
32 | ? moduleExports.Linter
|
33 | : moduleExports
|
34 | }
|
35 |
|
36 | function getModuleFromRequire() {
|
37 | return getLinterFromModule(require("eslint/lib/linter"))
|
38 | }
|
39 |
|
40 | function 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 |
|
55 | function iterateESLintModules(fn) {
|
56 | if (!require.cache || Object.keys(require.cache).length === 0) {
|
57 |
|
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.
|
90 | If you think this is a bug, please file a report at https://github.com/BenoitZugmeyer/eslint-plugin-html/issues
|
91 |
|
92 | In 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 |
|
111 | function 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 |
|
126 | function patch(Linter) {
|
127 | const verifyMethodName = Linter.prototype._verifyWithoutProcessors
|
128 | ? "_verifyWithoutProcessors"
|
129 | : "verify"
|
130 | const verify = Linter.prototype[verifyMethodName]
|
131 |
|
132 |
|
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 |
|
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 |
|
231 | function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {
|
232 |
|
233 | const firstPassValues = []
|
234 |
|
235 | for (const codePart of codeParts) {
|
236 | verifyCodePart(codePart, {
|
237 | prepare(context) {
|
238 | const globalScope = context.getScope()
|
239 |
|
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 |
|
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 |
|
289 | function 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 |
|
297 |
|
298 |
|
299 | column: message.column || 1,
|
300 | })
|
301 |
|
302 |
|
303 | if (location) {
|
304 | Object.assign(message, location)
|
305 | message.source = codePart.getOriginalLine(location.line)
|
306 |
|
307 |
|
308 | if (message.fix && message.fix.range) {
|
309 | message.fix.range = [
|
310 | codePart.originalIndex(message.fix.range[0]) + bomOffset,
|
311 |
|
312 |
|
313 | codePart.originalIndex(message.fix.range[1] - 1) + 1 + bomOffset,
|
314 | ]
|
315 | }
|
316 |
|
317 |
|
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 | }
|