Bug 1229858: Add a preprocessor to convert XBL into JavaScript blocks for eslint. r=miker
authorDave Townsend <dtownsend@oxymoronical.com>
Wed, 02 Dec 2015 17:03:33 -0800
changeset 276064 6c9319ab4430fd2858325fd1546d8fcc8fe513d7
parent 276063 64fc74a36a1606092a3cf23b949773b0518908cb
child 276065 d4a2076b059234d5fbd9397bd6ba3c112181ae80
push id69030
push usercbook@mozilla.com
push dateThu, 10 Dec 2015 11:34:18 +0000
treeherdermozilla-inbound@39c8aabf9719 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1229858
milestone45.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 1229858: Add a preprocessor to convert XBL into JavaScript blocks for eslint. r=miker
python/mach_commands.py
testing/eslint-plugin-mozilla/lib/index.js
testing/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
testing/eslint-plugin-mozilla/package.json
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -167,18 +167,18 @@ class MachCommands(MachCommandBase):
     @Command('eslint', category='devenv',
         description='Run eslint or help configure eslint for optimal development.')
     @CommandArgument('-s', '--setup', default=False, action='store_true',
         help='configure eslint for optimal development.')
     @CommandArgument('path', nargs='?', default='.',
         help='Path to files to lint, like "browser/components/loop" '
             'or "mobile/android". '
             'Defaults to the current directory if not given.')
-    @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx]',
-        help='Filename extensions to lint, default: "[.js,.jsm,.jsx]".')
+    @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml]',
+        help='Filename extensions to lint, default: "[.js,.jsm,.jsx,.xml]".')
     @CommandArgument('-b', '--binary', default=None,
         help='Path to eslint binary.')
     @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
     def eslint(self, setup, path, ext=None, binary=None, args=[]):
         '''Run eslint.'''
 
         if setup:
             return self.eslint_setup()
--- a/testing/eslint-plugin-mozilla/lib/index.js
+++ b/testing/eslint-plugin-mozilla/lib/index.js
@@ -8,16 +8,19 @@
 
 "use strict";
 
 //------------------------------------------------------------------------------
 // Plugin Definition
 //------------------------------------------------------------------------------
 
 module.exports = {
+  processors: {
+    ".xml": require("../lib/processors/xbl-bindings"),
+  },
   rules: {
     "balanced-listeners": require("../lib/rules/balanced-listeners"),
     "components-imports": require("../lib/rules/components-imports"),
     "import-headjs-globals": require("../lib/rules/import-headjs-globals"),
     "mark-test-function-used": require("../lib/rules/mark-test-function-used"),
     "no-aArgs": require("../lib/rules/no-aArgs"),
     "no-cpows-in-tests": require("../lib/rules/no-cpows-in-tests"),
     "var-only-at-top-level": require("../lib/rules/var-only-at-top-level")
new file mode 100644
--- /dev/null
+++ b/testing/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
@@ -0,0 +1,249 @@
+/**
+ * @fileoverview Converts functions and handlers from XBL bindings 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";
+
+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])
+  }
+}
+
+// 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.oncdata = this.onText.bind(this);
+
+  this.document = {
+    local: "#document",
+    uri: null,
+    children: [],
+  }
+  this._currentNode = this.document;
+}
+
+XMLParser.prototype = {
+  parser: null,
+
+  onOpenTag: function(tag) {
+    let node = {
+      parentNode: this._currentNode,
+      local: tag.local,
+      namespace: tag.uri,
+      attributes: {},
+      children: [],
+      textContent: "",
+      textStart: 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: function(tagname) {
+    this._currentNode = this._currentNode.parentNode;
+  },
+
+  onText: function(text) {
+    this._currentNode.textContent += text;
+  }
+}
+
+// Strips the indentation from lines of text and adds a fixed two spaces indent
+function reindent(text) {
+  let lines = text.split("\n");
+
+  // The last line is likely indentation for the XML closing tag.
+  if (lines[lines.length - 1].trim() == "") {
+    lines.pop();
+  }
+
+  if (!lines.length) {
+    return "";
+  }
+
+  // Find the preceeding 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(...indents);
+
+  // Strip off the found indent level and prepend the new indent level, but only
+  // if the string isn't already empty.
+  lines = lines.map(s => s.length > 0 ? "  " + s.substring(minIndent) : s);
+  return lines.join("\n") + "\n";
+}
+
+// -----------------------------------------------------------------------------
+// Processor Definition
+// -----------------------------------------------------------------------------
+
+// Stores any XML parse error
+let xmlParseError = null;
+
+// Stores the starting line for each script block generated
+let blockLines = [];
+
+module.exports = {
+  preprocess: function(text, filename) {
+    xmlParseError = null;
+    blockLines = [];
+
+    // 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 [];
+    }
+
+    let scripts = [];
+
+    for (let binding of bindings.children) {
+      if (binding.local != "binding" || binding.namespace != NS_XBL) {
+        continue;
+      }
+
+      for (let part of binding.children) {
+        if (part.namespace != NS_XBL) {
+          continue;
+        }
+
+        if (part.local != "implementation" && part.local != "handlers") {
+          continue;
+        }
+
+        for (let item of part.children) {
+          if (item.namespace != NS_XBL) {
+            continue;
+          }
+
+          switch (item.local) {
+            case "field": {
+              // Fields get converted into variable declarations
+              let def = item.textContent.trimRight();
+              // Ignore empty fields
+              if (def.trim().length == 0) {
+                continue;
+              }
+              blockLines.push(item.textStart);
+              scripts.push(`let ${item.attributes.name} = ${def}\n`);
+              break;
+            }
+            case "constructor":
+            case "destructor": {
+              // Constructors and destructors become function declarations
+              blockLines.push(item.textStart);
+              let content = reindent(item.textContent);
+              scripts.push(`function ${item.local}() {${content}}\n`);
+              break;
+            }
+            case "method": {
+              // Methods become function declarations with the appropriate params
+              let params = item.children.filter(n => n.local == "parameter" && n.namespace == NS_XBL)
+                                        .map(n => n.attributes.name)
+                                        .join(", ");
+              let body = item.children.filter(n => n.local == "body" && n.namespace == NS_XBL)[0];
+              blockLines.push(body.textStart);
+              body = reindent(body.textContent);
+              scripts.push(`function ${item.attributes.name}(${params}) {${body}}\n`)
+              break;
+            }
+            case "property": {
+              // Properties become one or two function declarations
+              for (let propdef of item.children) {
+                if (propdef.namespace != NS_XBL) {
+                  continue;
+                }
+
+                blockLines.push(propdef.textStart);
+                let content = reindent(propdef.textContent);
+                let params = propdef.local == "setter" ? "val" : "";
+                scripts.push(`function ${item.attributes.name}_${propdef.local}(${params}) {${content}}\n`);
+              }
+              break;
+            }
+            case "handler": {
+              // Handlers become a function declaration with an `event` parameter
+              blockLines.push(item.textStart);
+              let content = reindent(item.textContent);
+              scripts.push(`function on${item.attributes.event}(event) {${content}}\n`);
+              break;
+            }
+            default:
+              continue;
+          }
+        }
+      }
+    }
+
+    return scripts;
+  },
+
+  postprocess: function(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++) {
+      let line = blockLines[i];
+
+      for (let message of messages[i]) {
+        message.line += line;
+        errors.push(message);
+      }
+    }
+
+    return errors;
+  }
+};
--- a/testing/eslint-plugin-mozilla/package.json
+++ b/testing/eslint-plugin-mozilla/package.json
@@ -12,15 +12,16 @@
   "bugs": {
     "url": "https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Developer%20Tools"
   },
   "homepage": "https://bugzilla.mozilla.org/show_bug.cgi?id=1203520",
   "author": "Mike Ratcliffe",
   "main": "lib/index.js",
   "dependencies": {
     "escope": "^3.2.0",
-    "espree": "^2.2.4"
+    "espree": "^2.2.4",
+    "sax": "^1.1.4"
   },
   "engines": {
     "node": ">=0.10.0"
   },
   "license": "MPL-2.0"
 }