Bug 1537622 - add rudimentary support for linting XUL files, r=Standard8
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 05 Apr 2019 12:40:23 +0000
changeset 526977 3bf1176d151b6d5d2b6a84b41b3181af9da3c50b
parent 526976 f8891238701098a0ec340357c2095e037825f47e
child 526978 c47de582c3267a2c75244f60cd19ff02178f0bef
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1537622
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1537622 - add rudimentary support for linting XUL files, r=Standard8 Differential Revision: https://phabricator.services.mozilla.com/D24298
tools/lint/eslint.yml
tools/lint/eslint/eslint-plugin-mozilla/lib/index.js
tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js
tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js
tools/lint/eslint/eslint-plugin-mozilla/package-lock.json
tools/lint/eslint/eslint-plugin-mozilla/package.json
--- a/tools/lint/eslint.yml
+++ b/tools/lint/eslint.yml
@@ -1,15 +1,15 @@
 ---
 eslint:
     description: JavaScript linter
     # ESLint infra handles its own path filtering, so just include cwd
     include: ['.']
     exclude: []
-    extensions: ['js', 'jsm', 'jsx', 'xml', 'html', 'xhtml']
+    extensions: ['js', 'jsm', 'jsx', 'xml', 'xul', 'html', 'xhtml']
     support-files:
         - '**/.eslintrc.js'
         - '.eslintignore'
         - 'tools/lint/eslint/**'
         # Files that can influence global variables
         - 'browser/base/content/nsContextMenu.js'
         - 'browser/base/content/utilityOverlay.js'
         - 'browser/components/customizableui/content/panelUI.js'
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js
@@ -24,16 +24,17 @@ module.exports = {
     "chrome-worker": require("../lib/environments/chrome-worker.js"),
     "frame-script": require("../lib/environments/frame-script.js"),
     "jsm": require("../lib/environments/jsm.js"),
     "simpletest": require("../lib/environments/simpletest.js"),
     "privileged": require("../lib/environments/privileged.js"),
   },
   processors: {
     ".xml": require("../lib/processors/xbl-bindings"),
+    ".xul": require("../lib/processors/xul"),
   },
   rules: {
     "avoid-Date-timing": require("../lib/rules/avoid-Date-timing"),
     "avoid-removeChild": require("../lib/rules/avoid-removeChild"),
     "balanced-listeners": require("../lib/rules/balanced-listeners"),
     "consistent-if-bracing": require("../lib/rules/consistent-if-bracing"),
     "import-browser-window-globals":
       require("../lib/rules/import-browser-window-globals"),
copy from tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
copy to tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js
@@ -1,21 +1,15 @@
-/**
- * @fileoverview Converts functions and handlers from XBL bindings into JS
- * functions
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
+/* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 "use strict";
 
-const NS_XBL = "http://www.mozilla.org/xbl";
-
 let sax = require("sax");
 
 // Converts sax's error message to something that eslint will understand
 let errorRegex = /(.*)\nLine: (\d+)\nColumn: (\d+)\nChar: (.*)/;
 function parseError(err) {
   let matches = err.message.match(errorRegex);
   if (!matches) {
     return null;
@@ -27,37 +21,53 @@ function parseError(err) {
     line: parseInt(matches[2]) + 1,
     column: parseInt(matches[3]),
   };
 }
 
 let entityRegex = /&[\w][\w-\.]*;/g;
 
 // A simple sax listener that generates a tree of element information
-function XMLParser(parser) {
+function XMLParser(text) {
+  // Non-strict allows us to ignore many errors from entities and
+  // preprocessing at the expense of failing to report some XML errors.
+  // Unfortunately it also throws away the case of tagnames and attributes
+  let parser = sax.parser(false, {
+    lowercase: true,
+    xmlns: true,
+  });
+
+  parser.onerror = function(err) {
+    this.lastError = parseError(err);
+  };
+
   this.parser = parser;
   parser.onopentag = this.onOpenTag.bind(this);
   parser.onclosetag = this.onCloseTag.bind(this);
   parser.ontext = this.onText.bind(this);
   parser.onopencdata = this.onOpenCDATA.bind(this);
   parser.oncdata = this.onCDATA.bind(this);
   parser.oncomment = this.onComment.bind(this);
 
   this.document = {
     local: "#document",
     uri: null,
     children: [],
     comments: [],
   };
   this._currentNode = this.document;
+
+  parser.write(text);
 }
 
 XMLParser.prototype = {
   parser: null,
 
+  lastError: null,
+
   onOpenTag(tag) {
     let node = {
       parentNode: this._currentNode,
       local: tag.local,
       namespace: tag.uri,
       attributes: {},
       children: [],
       comments: [],
@@ -100,284 +110,11 @@ XMLParser.prototype = {
     this.addText(text);
   },
 
   onComment(text) {
     this._currentNode.comments.push(text);
   },
 };
 
-// -----------------------------------------------------------------------------
-// Processor Definition
-// -----------------------------------------------------------------------------
-
-// Stores any XML parse error
-let xmlParseError = null;
-
-// Stores the lines of JS code generated from the XBL
-let scriptLines = [];
-// Stores a map from the synthetic line number to the real line number
-// and column offset.
-let lineMap = [];
-
-function addSyntheticLine(line, linePos, addDisableLine) {
-  lineMap[scriptLines.length] = { line: linePos, offset: null };
-  scriptLines.push(line + (addDisableLine ? "" : " // eslint-disable-line"));
-}
-
-/**
- * Adds generated lines from an XBL node to the script to be passed back to
- * eslint.
- */
-function addNodeLines(node, reindent) {
-  let lines = node.textContent.split("\n");
-  let startLine = node.textLine;
-  let startColumn = node.textColumn;
-
-  // The case where there are no whitespace lines before the first text is
-  // treated differently for indentation
-  let indentFirst = false;
-
-  // Strip off any preceding whitespace only lines. These are often used to
-  // format the XML and CDATA blocks.
-  while (lines.length && lines[0].trim() == "") {
-    indentFirst = true;
-    startLine++;
-    lines.shift();
-  }
-
-  // Remember the indent of the last blank line, which is the closing part of
-  // the CDATA block.
-  let lastIndent = 0;
-  if (lines.length && lines[lines.length - 1].trim() == "") {
-    lastIndent = lines[lines.length - 1].length;
-  }
-
-  // If the second to last line is also blank and has a higher indent than the
-  // last one, then the CDATA block doesn't close with the closing tag.
-  if (lines.length > 2 && lines[lines.length - 2].trim() == "" &&
-      lines[lines.length - 2].length > lastIndent) {
-    lastIndent = lines[lines.length - 2].length;
-  }
-
-  // Strip off any whitespace lines at the end. These are often used to line
-  // up the closing tags
-  while (lines.length && lines[lines.length - 1].trim() == "") {
-    lines.pop();
-  }
-
-  if (!indentFirst) {
-    let firstLine = lines.shift();
-    // ESLint counts columns starting at 1 rather than 0
-    lineMap[scriptLines.length] = { line: startLine, offset: startColumn - 1 };
-    scriptLines.push(firstLine);
-    startLine++;
-  }
-
-  // Find the preceding whitespace for all lines that aren't entirely
-  // whitespace.
-  let indents = lines.filter(s => s.trim().length > 0)
-                     .map(s => s.length - s.trimLeft().length);
-  // Find the smallest indent level in use
-  let minIndent = Math.min.apply(null, indents);
-  let indent = Math.max(2, minIndent - lastIndent);
-
-  for (let line of lines) {
-    if (line.trim().length == 0) {
-      // Don't offset lines that are only whitespace, the only possible JS error
-      // is trailing whitespace and we want it to point at the right place
-      lineMap[scriptLines.length] = { line: startLine, offset: 0 };
-    } else {
-      line = " ".repeat(indent) + line.substring(minIndent);
-      lineMap[scriptLines.length] = {
-        line: startLine,
-        offset: 1 + indent - minIndent,
-      };
-    }
-
-    scriptLines.push(line);
-    startLine++;
-  }
-}
-
 module.exports = {
-  preprocess(text, filename) {
-    xmlParseError = null;
-    scriptLines = [];
-    lineMap = [];
-
-    // Non-strict allows us to ignore many errors from entities and
-    // preprocessing at the expense of failing to report some XML errors.
-    // Unfortunately it also throws away the case of tagnames and attributes
-    let parser = sax.parser(false, {
-      lowercase: true,
-      xmlns: true,
-    });
-
-    parser.onerror = function(err) {
-      xmlParseError = parseError(err);
-    };
-
-    let xp = new XMLParser(parser);
-    parser.write(text);
-
-    // Sanity checks to make sure we're dealing with an XBL document
-    let document = xp.document;
-    if (document.children.length != 1) {
-      return [];
-    }
-
-    let bindings = document.children[0];
-    if (bindings.local != "bindings" || bindings.namespace != NS_XBL) {
-      return [];
-    }
-
-    for (let comment of document.comments) {
-      addSyntheticLine(`/*`, 0, true);
-      for (let line of comment.split("\n")) {
-        addSyntheticLine(`${line.trim()}`, 0, true);
-      }
-      addSyntheticLine(`*/`, 0, true);
-    }
-
-    addSyntheticLine(`this.bindings = {`, bindings.textLine);
-
-    for (let binding of bindings.children) {
-      if (binding.local != "binding" || binding.namespace != NS_XBL) {
-        continue;
-      }
-
-      addSyntheticLine(`"${binding.attributes.id}": {`, binding.textLine);
-
-      for (let part of binding.children) {
-        if (part.namespace != NS_XBL) {
-          continue;
-        }
-
-        if (part.local == "implementation") {
-          addSyntheticLine(`implementation: {`, part.textLine);
-        } else if (part.local == "handlers") {
-          addSyntheticLine(`handlers: [`, part.textLine);
-        } else {
-          continue;
-        }
-
-        for (let item of part.children) {
-          if (item.namespace != NS_XBL) {
-            continue;
-          }
-
-          switch (item.local) {
-            case "field": {
-              // Fields are something like lazy getter functions
-
-              // Ignore empty fields
-              if (item.textContent.trim().length == 0) {
-                continue;
-              }
-
-              addSyntheticLine(`get ${item.attributes.name}() {`, item.textLine);
-              addSyntheticLine(`return (`, item.textLine);
-
-              // Remove trailing semicolons, as we are adding our own
-              item.textContent = item.textContent.replace(/;(?=\s*$)/, "");
-              addNodeLines(item, 5);
-
-              addSyntheticLine(`);`, item.textLine);
-              addSyntheticLine(`},`, item.textEndLine);
-              break;
-            }
-            case "constructor":
-            case "destructor": {
-              // Constructors and destructors become function declarations
-              addSyntheticLine(`${item.local}() {`, item.textLine);
-              addNodeLines(item, 4);
-              addSyntheticLine(`},`, item.textEndLine);
-              break;
-            }
-            case "method": {
-              // Methods become function declarations with the appropriate
-              // params.
-
-              let params = item.children.filter(n => {
-                return n.local == "parameter" && n.namespace == NS_XBL;
-              })
-                                        .map(n => n.attributes.name)
-                                        .join(", ");
-              let body = item.children.filter(n => {
-                return n.local == "body" && n.namespace == NS_XBL;
-              })[0];
-
-              addSyntheticLine(`${item.attributes.name}(${params}) {`, item.textLine);
-              addNodeLines(body, 4);
-              addSyntheticLine(`},`, item.textEndLine);
-              break;
-            }
-            case "property": {
-              // Properties become one or two function declarations
-              for (let propdef of item.children) {
-                if (propdef.namespace != NS_XBL) {
-                  continue;
-                }
-
-                if (propdef.local == "setter") {
-                  addSyntheticLine(`set ${item.attributes.name}(val) {`, propdef.textLine);
-                } else if (propdef.local == "getter") {
-                  addSyntheticLine(`get ${item.attributes.name}() {`, propdef.textLine);
-                } else {
-                  continue;
-                }
-                addNodeLines(propdef, 4);
-                addSyntheticLine(`},`, propdef.textEndLine);
-              }
-              break;
-            }
-            case "handler": {
-              // Handlers become a function declaration with an `event`
-              // parameter.
-              addSyntheticLine(`function(event) {`, item.textLine);
-              addNodeLines(item, 4);
-              addSyntheticLine(`},`, item.textEndLine);
-              break;
-            }
-            default:
-              continue;
-          }
-        }
-
-        addSyntheticLine((part.local == "implementation" ? `},` : `],`), part.textEndLine);
-      }
-      addSyntheticLine(`},`, binding.textEndLine);
-    }
-    addSyntheticLine(`};`, bindings.textEndLine);
-
-    let script = scriptLines.join("\n") + "\n";
-    return [script];
-  },
-
-  postprocess(messages, filename) {
-    // If there was an XML parse error then just return that
-    if (xmlParseError) {
-      return [xmlParseError];
-    }
-
-    // For every message from every script block update the line to point to the
-    // correct place.
-    let errors = [];
-    for (let i = 0; i < messages.length; i++) {
-      for (let message of messages[i]) {
-        // ESLint indexes lines starting at 1 but our arrays start at 0
-        let mapped = lineMap[message.line - 1];
-
-        message.line = mapped.line + 1;
-        if (mapped.offset) {
-          message.column -= mapped.offset;
-        } else {
-          message.column = NaN;
-        }
-
-        errors.push(message);
-      }
-    }
-
-    return errors;
-  },
+  XMLParser,
 };
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
@@ -6,109 +6,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 "use strict";
 
 const NS_XBL = "http://www.mozilla.org/xbl";
 
-let sax = require("sax");
-
-// Converts sax's error message to something that eslint will understand
-let errorRegex = /(.*)\nLine: (\d+)\nColumn: (\d+)\nChar: (.*)/;
-function parseError(err) {
-  let matches = err.message.match(errorRegex);
-  if (!matches) {
-    return null;
-  }
-
-  return {
-    fatal: true,
-    message: matches[1],
-    line: parseInt(matches[2]) + 1,
-    column: parseInt(matches[3]),
-  };
-}
-
-let entityRegex = /&[\w][\w-\.]*;/g;
-
-// A simple sax listener that generates a tree of element information
-function XMLParser(parser) {
-  this.parser = parser;
-  parser.onopentag = this.onOpenTag.bind(this);
-  parser.onclosetag = this.onCloseTag.bind(this);
-  parser.ontext = this.onText.bind(this);
-  parser.onopencdata = this.onOpenCDATA.bind(this);
-  parser.oncdata = this.onCDATA.bind(this);
-  parser.oncomment = this.onComment.bind(this);
-
-  this.document = {
-    local: "#document",
-    uri: null,
-    children: [],
-    comments: [],
-  };
-  this._currentNode = this.document;
-}
-
-XMLParser.prototype = {
-  parser: null,
-
-  onOpenTag(tag) {
-    let node = {
-      parentNode: this._currentNode,
-      local: tag.local,
-      namespace: tag.uri,
-      attributes: {},
-      children: [],
-      comments: [],
-      textContent: "",
-      textLine: this.parser.line,
-      textColumn: this.parser.column,
-      textEndLine: this.parser.line,
-    };
-
-    for (let attr of Object.keys(tag.attributes)) {
-      if (tag.attributes[attr].uri == "") {
-        node.attributes[attr] = tag.attributes[attr].value;
-      }
-    }
-
-    this._currentNode.children.push(node);
-    this._currentNode = node;
-  },
-
-  onCloseTag(tagname) {
-    this._currentNode.textEndLine = this.parser.line;
-    this._currentNode = this._currentNode.parentNode;
-  },
-
-  addText(text) {
-    this._currentNode.textContent += text;
-  },
-
-  onText(text) {
-    // Replace entities with some valid JS token.
-    this.addText(text.replace(entityRegex, "null"));
-  },
-
-  onOpenCDATA() {
-    // Turn the CDATA opening tag into whitespace for indent alignment
-    this.addText(" ".repeat("<![CDATA[".length));
-  },
-
-  onCDATA(text) {
-    this.addText(text);
-  },
-
-  onComment(text) {
-    this._currentNode.comments.push(text);
-  },
-};
+let XMLParser = require("./processor-helpers").XMLParser;
 
 // -----------------------------------------------------------------------------
 // Processor Definition
 // -----------------------------------------------------------------------------
 
 // Stores any XML parse error
 let xmlParseError = null;
 
@@ -199,30 +107,20 @@ function addNodeLines(node, reindent) {
 }
 
 module.exports = {
   preprocess(text, filename) {
     xmlParseError = null;
     scriptLines = [];
     lineMap = [];
 
-    // Non-strict allows us to ignore many errors from entities and
-    // preprocessing at the expense of failing to report some XML errors.
-    // Unfortunately it also throws away the case of tagnames and attributes
-    let parser = sax.parser(false, {
-      lowercase: true,
-      xmlns: true,
-    });
-
-    parser.onerror = function(err) {
-      xmlParseError = parseError(err);
-    };
-
-    let xp = new XMLParser(parser);
-    parser.write(text);
+    let xp = new XMLParser(text);
+    if (xp.lastError) {
+      xmlParseError = xp.lastError;
+    }
 
     // Sanity checks to make sure we're dealing with an XBL document
     let document = xp.document;
     if (document.children.length != 1) {
       return [];
     }
 
     let bindings = document.children[0];
new file mode 100644
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js
@@ -0,0 +1,253 @@
+/**
+ * @fileoverview Converts inline attributes from XUL into JS
+ * functions
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+let path = require("path");
+let fs = require("fs");
+
+let XMLParser = require("./processor-helpers").XMLParser;
+
+// Stores any XML parse error
+let xmlParseError = null;
+
+// Stores the lines of JS code generated from the XBL
+let scriptLines = [];
+// Stores a map from the synthetic line number to the real line number
+// and column offset.
+let lineMap = [];
+
+let includedRanges = [];
+
+// Deal with ifdefs. This is the state we pretend to have:
+const kIfdefStateForLinting = {
+  MOZ_UPDATER: true,
+  XP_WIN: true,
+  MOZ_BUILD_APP_IS_BROWSER: true,
+  MOZ_SERVICES_SYNC: true,
+  MOZ_DATA_REPORTING: true,
+  MOZ_TELEMETRY_REPORTING: true,
+  MOZ_CRASHREPORTER: true,
+  MOZ_MAINTENANCE_SERVICE: true,
+  HAVE_SHELL_SERVICE: true,
+  MENUBAR_CAN_AUTOHIDE: true,
+  MOZILLA_OFFICIAL: true,
+  BROWSER_XHTML: false, // other code can lint browser.xhtml, we hope.
+};
+
+// Anything not in the above list is assumed false.
+function dealWithIfdefs(text, filename) {
+  function stripIfdefsFromLines(input, innerFile) {
+    let outputLines = [];
+    let inSkippingIfdef = [false];
+    for (let i = 0; i < input.length; i++) {
+      let line = input[i];
+      let shouldSkip = inSkippingIfdef.some(x => x);
+      if (!line.startsWith("#")) {
+        outputLines.push(shouldSkip ? "" : line);
+      } else {
+        if (line.startsWith("# ") || line.startsWith("#filter") ||
+            line == "#" || line.startsWith("#define")) {
+          outputLines.push("");
+          continue;
+        }
+        // if this isn't just a comment (which we skip), figure out what to do:
+        let term = "";
+        let negate = false;
+        if (line.startsWith("#ifdef")) {
+          term = line.match(/^#ifdef *([A-Z_]+)/);
+        } else if (line.startsWith("#ifndef")) {
+          term = line.match(/^#ifndef *([A-Z_]+)/);
+          negate = true;
+        } else if (line.startsWith("#if ")) {
+          term = line.match(/^defined\(([A-Z_]+)\)/);
+        } else if (line.startsWith("#elifdef")) {
+          // Replace the old one:
+          inSkippingIfdef.pop();
+          term = line.match(/^#elifdef *([A-Z_]+)/);
+        } else if (line.startsWith("#else")) {
+          // Switch the last one around:
+          let old = inSkippingIfdef.pop();
+          inSkippingIfdef.push(!old);
+          outputLines.push("");
+        } else if (line.startsWith("#endif")) {
+          inSkippingIfdef.pop();
+          outputLines.push("");
+        } else if (line.startsWith("#expand")) {
+          // Just strip expansion instructions
+          outputLines.push(line.substring("#expand ".length));
+        } else if (line.startsWith("#include")) {
+          // Uh oh.
+          if (!shouldSkip) {
+            let fileToInclude = line.substr("#include ".length).trim();
+            let subpath = path.join(path.dirname(innerFile), fileToInclude);
+            let contents = fs.readFileSync(subpath, {encoding: "utf-8"});
+            contents = contents.split(/\n/);
+            // Recurse:
+            contents = stripIfdefsFromLines(contents, subpath);
+            if (innerFile == filename) {
+              includedRanges.push({
+                start: i,
+                end: i + contents.length,
+                filename: subpath,
+              });
+            }
+            // And insert the resulting lines:
+            input = input.slice(0, i).concat(contents, input.slice(i + 1));
+            // Re-process this line now that we've spliced things in.
+            i--;
+          } else {
+            outputLines.push("");
+          }
+        } else {
+          throw new Error("Unknown preprocessor directive: " + line);
+        }
+
+        if (term) {
+          // We always want the first capturing subgroup:
+          term = term && term[1];
+          if (!negate) {
+            inSkippingIfdef.push(!kIfdefStateForLinting[term]);
+          } else {
+            inSkippingIfdef.push(kIfdefStateForLinting[term]);
+          }
+          outputLines.push("");
+          // Now just continue; we'll include lines depending on the state of `inSkippingIfdef`.
+        }
+      }
+    }
+    return outputLines;
+  }
+  let lines = text.split(/\n/);
+  return stripIfdefsFromLines(lines, filename).join("\n");
+}
+
+function addSyntheticLine(line, linePos, addDisableLine) {
+  lineMap[scriptLines.length] = { line: linePos };
+  scriptLines.push(line + (addDisableLine ? "" : " // eslint-disable-line"));
+}
+
+function recursiveExpand(node) {
+  for (let [attr, value] of Object.entries(node.attributes)) {
+    if (attr.startsWith("on")) {
+      if (attr == "oncommand" && value == ";") {
+        // Ignore these, see bug 371900 for why people might do this.
+        continue;
+      }
+      let nodeDesc = node.local;
+      if (node.attributes.id) {
+        nodeDesc += "_" + node.attributes.id.replace(/[^a-z]/gi, "_");
+      }
+      if (node.attributes.class) {
+        nodeDesc += "_" + node.attributes.class.replace(/[^a-z]/gi, "_");
+      }
+      addSyntheticLine("function " + nodeDesc + "(event) {", node.textLine);
+      let processedLines = value.split(/\r?\n/);
+      let addlLine = 0;
+      for (let line of processedLines) {
+        line = line.replace(/^\s*/, "");
+        lineMap[scriptLines.length] = {
+          // Unfortunately, we only get a line number for the <tag> finishing,
+          // not for individual attributes.
+          line: node.textLine + addlLine,
+        };
+        scriptLines.push(line);
+        addlLine++;
+      }
+      addSyntheticLine("}", node.textLine + processedLines.length - 1);
+    }
+  }
+  for (let kid of node.children) {
+    recursiveExpand(kid);
+  }
+}
+
+module.exports = {
+  preprocess(text, filename) {
+    if (filename.includes(".inc")) {
+      return [];
+    }
+    xmlParseError = null;
+    // The following rules are annoying in XUL.
+    // Indent because in multiline attributes it's impossible to understand for the XML parser.
+    // Semicolons because those shouldn't be required for inline event handlers.
+    // Quotes because we use doublequotes for attributes so using single quotes
+    // for strings inside them makes sense.
+    // No-undef because it's a bunch of work to teach this code how to read
+    // scripts and get globals from them (though ideally we should do that at some point).
+    scriptLines = [
+      "/* eslint-disable indent */",
+      "/* eslint-disable indent-legacy */",
+      "/* eslint-disable semi */",
+      "/* eslint-disable quotes */",
+      "/* eslint-disable no-undef */",
+    ];
+    lineMap = scriptLines.map(() => ({line: 0}));
+    includedRanges = [];
+    // Do C-style preprocessing first:
+    text = dealWithIfdefs(text, filename);
+
+    let xp = new XMLParser(text);
+    if (xp.lastError) {
+      xmlParseError = xp.lastError;
+    }
+    let doc = xp.document;
+    if (!doc) {
+      return [];
+    }
+    let node = doc;
+    for (let kid of node.children) {
+      recursiveExpand(kid);
+    }
+
+    let scriptText = scriptLines.join("\n") + "\n";
+    return [scriptText];
+  },
+
+  postprocess(messages, filename) {
+    // If there was an XML parse error then just return that
+    if (xmlParseError) {
+      return [xmlParseError];
+    }
+
+    // For every message from every script block update the line to point to the
+    // correct place.
+    let errors = [];
+    for (let i = 0; i < messages.length; i++) {
+      for (let message of messages[i]) {
+        // ESLint indexes lines starting at 1 but our arrays start at 0
+        let mapped = lineMap[message.line - 1];
+        // Ensure we don't modify this by making a copy. We might need it for another failure.
+        let target = mapped.line;
+        let includedRange = includedRanges.find(r => (target >= r.start && target <= r.end));
+        // If this came from an #included file, indicate this in the message
+        if (includedRange) {
+          target = includedRange.start;
+          message.message += " (from included file " + path.basename(includedRange.filename) + ")";
+        }
+        // Compensate for line numbers shifting as a result of #include:
+        let includeBallooning =
+          includedRanges.filter(r => (target >= r.end))
+                        .map(r => r.end - r.start)
+                        .reduce((acc, next) => acc + next, 0);
+        target -= includeBallooning;
+        // Add back the 1 to go back to 1-indexing.
+        message.line = target + 1;
+
+        // We never have column information, unfortunately.
+        message.column = NaN;
+
+        errors.push(message);
+      }
+    }
+
+    return errors;
+  },
+};
+
--- a/tools/lint/eslint/eslint-plugin-mozilla/package-lock.json
+++ b/tools/lint/eslint/eslint-plugin-mozilla/package-lock.json
@@ -1,11 +1,11 @@
 {
   "name": "eslint-plugin-mozilla",
-  "version": "1.1.3",
+  "version": "1.2.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
     "@babel/code-frame": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
       "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
       "dev": true,
--- a/tools/lint/eslint/eslint-plugin-mozilla/package.json
+++ b/tools/lint/eslint/eslint-plugin-mozilla/package.json
@@ -1,11 +1,11 @@
 {
   "name": "eslint-plugin-mozilla",
-  "version": "1.1.3",
+  "version": "1.2.0",
   "description": "A collection of rules that help enforce JavaScript coding standard in the Mozilla project.",
   "keywords": [
     "eslint",
     "eslintplugin",
     "eslint-plugin",
     "mozilla",
     "firefox"
   ],