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 | const getFileMode = require("./getFileMode")
|
9 |
|
10 | const PREPARE_RULE_NAME = "__eslint-plugin-html-prepare"
|
11 | const LINTER_ISPATCHED_PROPERTY_NAME =
|
12 | "__eslint-plugin-html-verify-function-is-patched"
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | const needles = [
|
25 | path.join("lib", "linter", "linter.js"),
|
26 | path.join("lib", "linter.js"),
|
27 | ]
|
28 |
|
29 | iterateESLintModules(patch)
|
30 |
|
31 | function getLinterFromModule(moduleExports) {
|
32 | return moduleExports.Linter
|
33 | ? moduleExports.Linter
|
34 | : moduleExports
|
35 | }
|
36 |
|
37 | function getModuleFromRequire() {
|
38 | return getLinterFromModule(require("eslint/lib/linter"))
|
39 | }
|
40 |
|
41 | function 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 |
|
56 | function iterateESLintModules(fn) {
|
57 | if (!require.cache || Object.keys(require.cache).length === 0) {
|
58 |
|
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.
|
91 | If you think this is a bug, please file a report at https://github.com/BenoitZugmeyer/eslint-plugin-html/issues
|
92 |
|
93 | In 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 |
|
112 | function patch(Linter) {
|
113 | const verifyMethodName = Linter.prototype._verifyWithoutProcessors
|
114 | ? "_verifyWithoutProcessors"
|
115 | : "verify"
|
116 | const verify = Linter.prototype[verifyMethodName]
|
117 |
|
118 |
|
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 |
|
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 |
|
217 | function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {
|
218 |
|
219 | const firstPassValues = []
|
220 |
|
221 | for (const codePart of codeParts) {
|
222 | verifyCodePart(codePart, {
|
223 | prepare(context) {
|
224 | const globalScope = context.getScope()
|
225 |
|
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 |
|
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 |
|
275 | function 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 |
|
287 | function remapMessage(message, hasBOM, codePart) {
|
288 | if (!message.line || !message.column) {
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 | return true
|
296 | }
|
297 |
|
298 | const location = codePart.originalLocation({
|
299 | line: message.line,
|
300 | column: message.column,
|
301 | })
|
302 |
|
303 |
|
304 | if (!location) {
|
305 | return false
|
306 | }
|
307 |
|
308 | Object.assign(message, location)
|
309 | message.source = codePart.getOriginalLine(location.line)
|
310 |
|
311 |
|
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 |
|
317 |
|
318 | codePart.originalIndex(message.fix.range[1] - 1) + 1 + bomOffset,
|
319 | ]
|
320 | }
|
321 |
|
322 |
|
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 | }
|