Repository: eslint/eslint
Pull request #8269 (2017-03-17 00:59:03)

Upgrade: doctrine to 2.0.0
**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[X] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**
Upgrade doctrine to latest version

**Is there anything you'd like reviewers to focus on?**
No



eslintbot: LGTM
mention-bot: @alberto, thanks for your PR! By analyzing the history of the files in this pull request, we identified @mysticatea, @nzakas and @gyandeeps to be potential reviewers.

Patch:
diff --git a/docs/developer-guide/selectors.md b/docs/developer-guide/selectors.md
new file mode 100644
index 000000000..caa0fe043
--- /dev/null
+++ b/docs/developer-guide/selectors.md
@@ -0,0 +1,133 @@
+# Selectors
+
+Some rules and APIs allow the use of selectors to query an AST. This page is intended to:
+
+1. Explain what selectors are
+1. Describe the syntax for creating selectors
+1. Describe what selectors can be used for
+
+## What is a selector?
+
+A selector is a string that can be used to match nodes in an Abstract Syntax Tree (AST). This is useful for describing a particular syntax pattern in your code.
+
+The syntax for AST selectors is similar to the syntax for [CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). If you've used CSS selectors before, the syntax for AST selectors should be easy to understand.
+
+The simplest selector is just a node type. A node type selector will match all nodes with the given type. For example, consider the following program:
+
+```js
+var foo = 1;
+bar.baz();
+```
+
+The selector "`Identifier`" will match all `Identifier` nodes in the program. In this case, the selector will match the nodes for `foo`, `bar`, and `baz`.
+
+Selectors are not limited to matching against single node types. For example, the selector `VariableDeclarator > Identifier` will match all `Identifier` nodes that have a `VariableDeclarator` as a direct parent. In the program above, this will match the node for `foo`, but not the nodes for `bar` and `baz`.
+
+## What syntax can selectors have?
+
+The following selectors are supported:
+
+* AST node type: `ForStatement`
+* wildcard (matches all nodes): `*`
+* attribute existence: `[attr]`
+* attribute value: `[attr="foo"]` or `[attr=123]`
+* attribute regex: `[attr=/foo.*/]`
+* attribute conditons: `[attr!="foo"]`, `[attr>2]`, `[attr<3]`, `[attr>=2]`, or `[attr<=3]`
+* nested attribute: `[attr.level2="foo"]`
+* field: `FunctionDeclaration > Identifier.id`
+* First or last child: `:first-child` or `:last-child`
+* nth-child (no ax+b support): `:nth-child(2)`
+* nth-last-child (no ax+b support): `:nth-last-child(1)`
+* descendant: `FunctionExpression ReturnStatement`
+* child: `UnaryExpression > Literal`
+* following sibling: `ArrayExpression > Literal + SpreadElement`
+* adjacent sibling: `VariableDeclaration ~ VariableDeclaration`
+* negation: `:not(ForStatement)`
+* matches-any: `:matches([attr] > :first-child, :last-child)`
+* class of AST node: `:statement`, `:expression`, `:declaration`, `:function`, or `:pattern`
+
+This syntax is very powerful, and can be used to precisely select many syntactic patterns in your code.
+
+<sup>The examples in this section were adapted from the [esquery](https://github.com/estools/esquery) documentation.</sup>
+
+## What can selectors be used for?
+
+If you're writing custom ESLint rules, you might be interested in using selectors to examine specific parts of the AST. If you're configuring ESLint for your codebase, you might be interested in restricting particular syntax patterns with selectors.
+
+### Listening for selectors in rules
+
+When writing a custom ESLint rule, you can listen for nodes that match a particular selector as the AST is traversed.
+
+```js
+module.exports = {
+  create(context) {
+    // ...
+
+    return {
+
+      // This listener will be called for all IfStatement nodes with blocks.
+      "IfStatement > BlockStatement": function(blockStatementNode) {
+        // ...your logic here
+      },
+
+      // This listener will be called for all function declarations with more than 3 parameters.
+      "FunctionDeclaration[params.length>3]": function(functionDeclarationNode) {
+        // ...your logic here
+      }
+    };
+  }
+};
+```
+
+Adding `:exit` to the end of a selector will cause the listener to be called when the matching nodes are exited during traversal, rather than when they are entered.
+
+If two or more selectors match the same node, their listeners will be called in order of increasing specificity. The specificity of an AST selector is similar to the specificity of a CSS selector:
+
+* When comparing two selectors, the selector that contains more class selectors, attribute selectors, and pseudo-class selectors (excluding `:not()`) has higher specificity.
+* If the class/attribute/pseudo-class count is tied, the selector that contains more node type selectors has higher specificity.
+
+If multiple selectors have equal specificity, their listeners will be called in alphabetical order for that node.
+
+### Restricting syntax with selectors
+
+With the [no-restricted-syntax](/docs/rules/no-restricted-syntax) rule, you can restrict the usage of particular syntax in your code. For example, you can use the following configuration to disallow using `if` statements that do not have block statements as their body:
+
+```json
+{
+  "rules": {
+    "no-restricted-syntax": ["error", "IfStatement > :not(BlockStatement).body"]
+  }
+}
+```
+
+...or equivalently, you can use this configuration:
+
+```json
+{
+  "rules": {
+    "no-restricted-syntax": ["error", "IfStatement[body.type!='BlockStatement']"]
+  }
+}
+```
+
+As another example, you can disallow calls to `require()`:
+
+```json
+{
+  "rules": {
+    "no-restricted-syntax": ["error", "CallExpression[callee.name='require']"]
+  }
+}
+```
+
+Or you can enforce that calls to `setTimeout` always have two arguments:
+
+```json
+{
+  "rules": {
+    "no-restricted-syntax": ["error", "CallExpression[callee.name='setTimeout'][arguments.length!=2]"]
+  }
+}
+```
+
+Using selectors in the `no-restricted-syntax` rule can give you a lot of control over problematic patterns in your codebase, without needing to write custom rules to detect each pattern.
diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md
index a5d3727d1..a5d391fcd 100644
--- a/docs/developer-guide/working-with-rules.md
+++ b/docs/developer-guide/working-with-rules.md
@@ -65,8 +65,8 @@ The source file for a rule exports an object with the following properties.
 
 `create` (function) returns an object with methods that ESLint calls to "visit" nodes while traversing the abstract syntax tree (AST as defined by [ESTree](https://github.com/estree/estree)) of JavaScript code:
 
-* if a key is a node type, ESLint calls that **visitor** function while going **down** the tree
-* if a key is a node type plus `:exit`, ESLint calls that **visitor** function while going **up** the tree
+* if a key is a node type or a [selector](./selectors), ESLint calls that **visitor** function while going **down** the tree
+* if a key is a node type or a [selector](./selectors) plus `:exit`, ESLint calls that **visitor** function while going **up** the tree
 * if a key is an event name, ESLint calls that **handler** function for [code path analysis](./code-path-analysis.md)
 
 A rule can use the current node and its surrounding tree to report or fix problems.
diff --git a/docs/rules/no-restricted-syntax.md b/docs/rules/no-restricted-syntax.md
index 74f35628f..2e94e5d66 100644
--- a/docs/rules/no-restricted-syntax.md
+++ b/docs/rules/no-restricted-syntax.md
@@ -1,9 +1,11 @@
 # disallow specified syntax (no-restricted-syntax)
 
-JavaScript has a lot of language features, and not everyone likes all of them. As a result, some projects choose to disallow the use of certain language features altogether. For instance, you might decide to disallow the use of `try-catch` or `class`.
+JavaScript has a lot of language features, and not everyone likes all of them. As a result, some projects choose to disallow the use of certain language features altogether. For instance, you might decide to disallow the use of `try-catch` or `class`, or you might decide to disallow the use of the `in` operator.
 
 Rather than creating separate rules for every language feature you want to turn off, this rule allows you to configure the syntax elements you want to restrict use of. These elements are represented by their [ESTree](https://github.com/estree/estree) node types. For example, a function declaration is represented by `FunctionDeclaration` and the `with` statement is represented by `WithStatement`. You may find the full list of AST node names you can use [on GitHub](https://github.com/eslint/espree/blob/master/lib/ast-node-types.js) and use the [online parser](http://eslint.org/parser/) to see what type of nodes your code consists of.
 
+You can also specify [AST selectors](../developer-guide/selectors) to restrict, allowing much more precise control over syntax patterns.
+
 ## Rule Details
 
 This rule disallows specified (that is, user-defined) syntax.
@@ -15,31 +17,35 @@ This rule takes a list of strings:
 ```json
 {
     "rules": {
-        "no-restricted-syntax": ["error", "FunctionExpression", "WithStatement"]
+        "no-restricted-syntax": ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"]
     }
 }
 ```
 
-Examples of **incorrect** code for this rule with the `"FunctionExpression", "WithStatement"` options:
+Examples of **incorrect** code for this rule with the `"FunctionExpression", "WithStatement", BinaryExpression[operator='in']` options:
 
 ```js
-/* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement"] */
+/* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] */
 
 with (me) {
     dontMess();
 }
 
 var doSomething = function () {};
+
+foo in bar;
 ```
 
-Examples of **correct** code for this rule with the `"FunctionExpression", "WithStatement"` options:
+Examples of **correct** code for this rule with the `"FunctionExpression", "WithStatement", BinaryExpression[operator='in']` options:
 
 ```js
-/* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement"] */
+/* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] */
 
 me.dontMess();
 
 function doSomething() {};
+
+foo instanceof bar;
 ```
 
 ## When Not To Use It
diff --git a/lib/eslint.js b/lib/eslint.js
index 01df167fb..3c23a7495 100755
--- a/lib/eslint.js
+++ b/lib/eslint.js
@@ -867,11 +867,11 @@ module.exports = (function() {
                     const rule = ruleCreator.create ? ruleCreator.create(ruleContext)
                         : ruleCreator(ruleContext);
 
-                    // add all the node types as listeners
-                    Object.keys(rule).forEach(nodeType => {
-                        api.on(nodeType, timing.enabled
-                            ? timing.time(key, rule[nodeType])
-                            : rule[nodeType]
+                    // add all the selectors from the rule as listeners
+                    Object.keys(rule).forEach(selector => {
+                        api.on(selector, timing.enabled
+                            ? timing.time(key, rule[selector])
+                            : rule[selector]
                         );
                     });
                 } catch (ex) {
diff --git a/lib/rules/no-new-func.js b/lib/rules/no-new-func.js
index 17ca7c9f0..6abbe8391 100644
--- a/lib/rules/no-new-func.js
+++ b/lib/rules/no-new-func.js
@@ -27,20 +27,18 @@ module.exports = {
         //--------------------------------------------------------------------------
 
         /**
-         * Checks if the callee is the Function constructor, and if so, reports an issue.
-         * @param {ASTNode} node The node to check and report on
+         * Reports a node.
+         * @param {ASTNode} node The node to report
          * @returns {void}
          * @private
          */
-        function validateCallee(node) {
-            if (node.callee.name === "Function") {
-                context.report({ node, message: "The Function constructor is eval." });
-            }
+        function report(node) {
+            context.report({ node, message: "The Function constructor is eval." });
         }
 
         return {
-            NewExpression: validateCallee,
-            CallExpression: validateCallee
+            "NewExpression[callee.name = 'Function']": report,
+            "CallExpression[callee.name = 'Function']": report
         };
 
     }
diff --git a/lib/rules/no-new.js b/lib/rules/no-new.js
index e0f45de1b..6e6025aac 100644
--- a/lib/rules/no-new.js
+++ b/lib/rules/no-new.js
@@ -24,12 +24,8 @@ module.exports = {
     create(context) {
 
         return {
-
-            ExpressionStatement(node) {
-
-                if (node.expression.type === "NewExpression") {
-                    context.report({ node, message: "Do not use 'new' for side effects." });
-                }
+            "ExpressionStatement > NewExpression"(node) {
+                context.report({ node: node.parent, message: "Do not use 'new' for side effects." });
             }
         };
 
diff --git a/lib/rules/no-process-exit.js b/lib/rules/no-process-exit.js
index c0c245554..04e423b88 100644
--- a/lib/rules/no-process-exit.js
+++ b/lib/rules/no-process-exit.js
@@ -26,17 +26,9 @@ module.exports = {
         //--------------------------------------------------------------------------
 
         return {
-
-            CallExpression(node) {
-                const callee = node.callee;
-
-                if (callee.type === "MemberExpression" && callee.object.name === "process" &&
-                    callee.property.name === "exit"
-                ) {
-                    context.report({ node, message: "Don't use process.exit(); throw an error instead." });
-                }
+            "CallExpression > MemberExpression.callee[object.name = 'process'][property.name = 'exit']"(node) {
+                context.report({ node: node.parent, message: "Don't use process.exit(); throw an error instead." });
             }
-
         };
 
     }
diff --git a/lib/rules/no-restricted-syntax.js b/lib/rules/no-restricted-syntax.js
index 830452d99..d87e589c7 100644
--- a/lib/rules/no-restricted-syntax.js
+++ b/lib/rules/no-restricted-syntax.js
@@ -8,8 +8,6 @@
 // Rule Definition
 //------------------------------------------------------------------------------
 
-const nodeTypes = require("espree").Syntax;
-
 module.exports = {
     meta: {
         docs: {
@@ -20,32 +18,18 @@ module.exports = {
 
         schema: {
             type: "array",
-            items: [
-                {
-                    enum: Object.keys(nodeTypes).map(k => nodeTypes[k])
-                }
-            ],
+            items: [{ type: "string" }],
             uniqueItems: true,
             minItems: 0
         }
     },
 
     create(context) {
-
-        /**
-         * Generates a warning from the provided node, saying that node type is not allowed.
-         * @param {ASTNode} node The node to warn on
-         * @returns {void}
-         */
-        function warn(node) {
-            context.report({ node, message: "Using '{{type}}' is not allowed.", data: node });
-        }
-
-        return context.options.reduce((result, nodeType) => {
-            result[nodeType] = warn;
-
-            return result;
-        }, {});
+        return context.options.reduce((result, selector) => Object.assign(result, {
+            [selector](node) {
+                context.report({ node, message: "Using '{{selector}}' is not allowed.", data: { selector } });
+            }
+        }), {});
 
     }
 };
diff --git a/lib/rules/no-sync.js b/lib/rules/no-sync.js
index 90b2be1e9..885b1eb62 100644
--- a/lib/rules/no-sync.js
+++ b/lib/rules/no-sync.js
@@ -26,19 +26,14 @@ module.exports = {
 
         return {
 
-            MemberExpression(node) {
-                const propertyName = node.property.name,
-                    syncRegex = /.*Sync$/;
-
-                if (syncRegex.exec(propertyName) !== null) {
-                    context.report({
-                        node,
-                        message: "Unexpected sync method: '{{propertyName}}'.",
-                        data: {
-                            propertyName
-                        }
-                    });
-                }
+            "MemberExpression[property.name=/.*Sync$/]"(node) {
+                context.report({
+                    node,
+                    message: "Unexpected sync method: '{{propertyName}}'.",
+                    data: {
+                        propertyName: node.property.name
+                    }
+                });
             }
         };
 
diff --git a/lib/util/node-event-generator.js b/lib/util/node-event-generator.js
index 1666ae93f..568a3b7fe 100644
--- a/lib/util/node-event-generator.js
+++ b/lib/util/node-event-generator.js
@@ -5,6 +5,185 @@
 
 "use strict";
 
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const esquery = require("esquery");
+const lodash = require("lodash");
+
+//------------------------------------------------------------------------------
+// Typedefs
+//------------------------------------------------------------------------------
+
+/**
+ * An object describing an AST selector
+ * @typedef {Object} ASTSelector
+ * @property {string} rawSelector The string that was parsed into this selector
+ * @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
+ * @property {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
+ * @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
+ * or `null` if all node types could cause a match
+ * @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
+ * @property {number} identifierCount The total number of identifier queries in this selector
+ */
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+* Gets the possible types of a selector
+* @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
+* @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
+*/
+function getPossibleTypes(parsedSelector) {
+    switch (parsedSelector.type) {
+        case "identifier":
+            return [parsedSelector.value];
+
+        case "matches": {
+            const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
+
+            if (typesForComponents.every(typesForComponent => typesForComponent)) {
+                return lodash.union.apply(null, typesForComponents);
+            }
+            return null;
+        }
+
+        case "compound": {
+            const typesForComponents = parsedSelector.selectors.map(getPossibleTypes).filter(typesForComponent => typesForComponent);
+
+            // If all of the components could match any type, then the compound could also match any type.
+            if (!typesForComponents.length) {
+                return null;
+            }
+
+            /*
+             * If at least one of the components could only match a particular type, the compound could only match
+             * the intersection of those types.
+             */
+            return lodash.intersection.apply(null, typesForComponents);
+        }
+
+        case "child":
+        case "descendant":
+        case "sibling":
+        case "adjacent":
+            return getPossibleTypes(parsedSelector.right);
+
+        default:
+            return null;
+
+    }
+}
+
+/**
+ * Counts the number of class, pseudo-class, and attribute queries in this selector
+ * @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
+ * @returns {number} The number of class, pseudo-class, and attribute queries in this selector
+ */
+function countClassAttributes(parsedSelector) {
+    switch (parsedSelector.type) {
+        case "child":
+        case "descendant":
+        case "sibling":
+        case "adjacent":
+            return countClassAttributes(parsedSelector.left) + countClassAttributes(parsedSelector.right);
+
+        case "compound":
+        case "not":
+        case "matches":
+            return parsedSelector.selectors.reduce((sum, childSelector) => sum + countClassAttributes(childSelector), 0);
+
+        case "attribute":
+        case "field":
+        case "nth-child":
+        case "nth-last-child":
+            return 1;
+
+        default:
+            return 0;
+    }
+}
+
+/**
+ * Counts the number of identifier queries in this selector
+ * @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
+ * @returns {number} The number of identifier queries
+ */
+function countIdentifiers(parsedSelector) {
+    switch (parsedSelector.type) {
+        case "child":
+        case "descendant":
+        case "sibling":
+        case "adjacent":
+            return countIdentifiers(parsedSelector.left) + countIdentifiers(parsedSelector.right);
+
+        case "compound":
+        case "not":
+        case "matches":
+            return parsedSelector.selectors.reduce((sum, childSelector) => sum + countIdentifiers(childSelector), 0);
+
+        case "identifier":
+            return 1;
+
+        default:
+            return 0;
+    }
+}
+
+/**
+ * Compares the specificity of two selector objects, with CSS-like rules.
+ * @param {ASTSelector} selectorA An AST selector descriptor
+ * @param {ASTSelector} selectorB Another AST selector descriptor
+ * @returns {number}
+ * a value less than 0 if selectorA is less specific than selectorB
+ * a value greater than 0 if selectorA is more specific than selectorB
+ * a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
+ * a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
+ */
+function compareSpecificity(selectorA, selectorB) {
+    return selectorA.attributeCount - selectorB.attributeCount ||
+        selectorA.identifierCount - selectorB.identifierCount ||
+        (selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1);
+}
+
+/**
+ * Parses a raw selector string, and throws a useful error if parsing fails.
+ * @param {string} rawSelector A raw AST selector
+ * @returns {Object} An object (from esquery) describing the matching behavior of this selector
+ * @throws {Error} An error if the selector is invalid
+ */
+function tryParseSelector(rawSelector) {
+    try {
+        return esquery.parse(rawSelector.replace(/:exit$/, ""));
+    } catch (err) {
+        if (typeof err.offset === "number") {
+            throw new Error(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`);
+        }
+        throw err;
+    }
+}
+
+/**
+ * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
+ * @param {string} rawSelector A raw AST selector
+ * @returns {ASTSelector} A selector descriptor
+ */
+const parseSelector = lodash.memoize(rawSelector => {
+    const parsedSelector = tryParseSelector(rawSelector);
+
+    return {
+        rawSelector,
+        isExit: rawSelector.endsWith(":exit"),
+        parsedSelector,
+        listenerTypes: getPossibleTypes(parsedSelector),
+        attributeCount: countClassAttributes(parsedSelector),
+        identifierCount: countIdentifiers(parsedSelector)
+    };
+});
+
 //------------------------------------------------------------------------------
 // Public Interface
 //------------------------------------------------------------------------------
@@ -24,10 +203,97 @@
 class NodeEventGenerator {
 
     /**
-     * @param {EventEmitter} emitter - An event emitter which is the destination of events.
-     */
+    * @param {EventEmitter} emitter - An event emitter which is the destination of events. This emitter must already
+    * have registered listeners for all of the events that it needs to listen for.
+    * @returns {NodeEventGenerator} new instance
+    */
     constructor(emitter) {
         this.emitter = emitter;
+        this.currentAncestry = [];
+        this.enterSelectorsByNodeType = new Map();
+        this.exitSelectorsByNodeType = new Map();
+        this.anyTypeEnterSelectors = [];
+        this.anyTypeExitSelectors = [];
+
+        const eventNames = typeof emitter.eventNames === "function"
+
+            // Use the built-in eventNames() function if available (Node 6+)
+            ? emitter.eventNames()
+
+            /*
+             * Otherwise, use the private _events property.
+             * Using a private property isn't ideal here, but this seems to
+             * be the best way to get a list of event names without overriding
+             * addEventListener, which would hurt performance. This property
+             * is widely used and unlikely to be removed in a future version
+             * (see https://github.com/nodejs/node/issues/1817). Also, future
+             * node versions will have eventNames() anyway.
+             */
+            : Object.keys(emitter._events); // eslint-disable-line no-underscore-dangle
+
+        eventNames.forEach(rawSelector => {
+            const selector = parseSelector(rawSelector);
+
+            if (selector.listenerTypes) {
+                selector.listenerTypes.forEach(nodeType => {
+                    const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;
+
+                    if (!typeMap.has(nodeType)) {
+                        typeMap.set(nodeType, []);
+                    }
+                    typeMap.get(nodeType).push(selector);
+                });
+            } else {
+                (selector.isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors).push(selector);
+            }
+        });
+
+        this.anyTypeEnterSelectors.sort(compareSpecificity);
+        this.anyTypeExitSelectors.sort(compareSpecificity);
+        this.enterSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
+        this.exitSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
+    }
+
+    /**
+     * Checks a selector against a node, and emits it if it matches
+     * @param {ASTNode} node The node to check
+     * @param {ASTSelector} selector An AST selector descriptor
+     * @returns {void}
+     */
+    applySelector(node, selector) {
+        if (esquery.matches(node, selector.parsedSelector, this.currentAncestry)) {
+            this.emitter.emit(selector.rawSelector, node);
+        }
+    }
+
+    /**
+     * Applies all appropriate selectors to a node, in specificity order
+     * @param {ASTNode} node The node to check
+     * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
+     * @returns {void}
+     */
+    applySelectors(node, isExit) {
+        const selectorsByNodeType = (isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType).get(node.type) || [];
+        const anyTypeSelectors = isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
+
+        /*
+         * selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
+         * Iterate through each of them, applying selectors in the right order.
+         */
+        let selectorsByTypeIndex = 0;
+        let anyTypeSelectorsIndex = 0;
+
+        while (selectorsByTypeIndex < selectorsByNodeType.length || anyTypeSelectorsIndex < anyTypeSelectors.length) {
+            if (
+                selectorsByTypeIndex >= selectorsByNodeType.length ||
+                anyTypeSelectorsIndex < anyTypeSelectors.length &&
+                compareSpecificity(anyTypeSelectors[anyTypeSelectorsIndex], selectorsByNodeType[selectorsByTypeIndex]) < 0
+            ) {
+                this.applySelector(node, anyTypeSelectors[anyTypeSelectorsIndex++]);
+            } else {
+                this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
+            }
+        }
     }
 
     /**
@@ -36,7 +302,10 @@ class NodeEventGenerator {
      * @returns {void}
      */
     enterNode(node) {
-        this.emitter.emit(node.type, node);
+        if (node.parent) {
+            this.currentAncestry.unshift(node.parent);
+        }
+        this.applySelectors(node, false);
     }
 
     /**
@@ -45,7 +314,8 @@ class NodeEventGenerator {
      * @returns {void}
      */
     leaveNode(node) {
-        this.emitter.emit(`${node.type}:exit`, node);
+        this.applySelectors(node, true);
+        this.currentAncestry.shift();
     }
 }
 
diff --git a/package.json b/package.json
index 4fd3423cc..bc9e24929 100644
--- a/package.json
+++ b/package.json
@@ -38,9 +38,10 @@
     "chalk": "^1.1.3",
     "concat-stream": "^1.5.2",
     "debug": "^2.1.1",
-    "doctrine": "^1.2.2",
+    "doctrine": "^2.0.0",
     "escope": "^3.6.0",
     "espree": "^3.4.0",
+    "esquery": "^1.0.0",
     "estraverse": "^4.2.0",
     "esutils": "^2.0.2",
     "file-entry-cache": "^2.0.0",


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index f3561c47a..61fbdf597 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -343,12 +343,13 @@ RuleTester.prototype = {
              * running the rule under test.
              */
             eslint.reset();
+
             eslint.on("Program", node => {
                 beforeAST = cloneDeeplyExcludesParent(node);
+            });
 
-                eslint.on("Program:exit", node => {
-                    afterAST = cloneDeeplyExcludesParent(node);
-                });
+            eslint.on("Program:exit", node => {
+                afterAST = node;
             });
 
             // Freezes rule-context properties.
@@ -385,7 +386,7 @@ RuleTester.prototype = {
                 return {
                     messages: eslint.verify(code, config, filename, true),
                     beforeAST,
-                    afterAST
+                    afterAST: cloneDeeplyExcludesParent(afterAST)
                 };
             } finally {
                 rules.get = originalGet;
diff --git a/tests/lib/rules/no-restricted-syntax.js b/tests/lib/rules/no-restricted-syntax.js
index 05dc3beca..7547e302b 100644
--- a/tests/lib/rules/no-restricted-syntax.js
+++ b/tests/lib/rules/no-restricted-syntax.js
@@ -21,7 +21,12 @@ ruleTester.run("no-restricted-syntax", rule, {
     valid: [
         { code: "doSomething();" },
         { code: "var foo = 42;", options: ["ConditionalExpression"] },
-        { code: "foo += 42;", options: ["VariableDeclaration", "FunctionExpression"] }
+        { code: "foo += 42;", options: ["VariableDeclaration", "FunctionExpression"] },
+        { code: "foo;", options: ["Identifier[name=\"bar\"]"] },
+        { code: "() => 5", options: ["ArrowFunctionExpression > BlockStatement"], parserOptions: { ecmaVersion: 6 } },
+        { code: "({ foo: 1, bar: 2 })", options: ["Property > Literal.key"] },
+        { code: "A: for (;;) break;", options: ["BreakStatement[label]"] },
+        { code: "function foo(bar, baz) {}", options: ["FunctionDeclaration[params.length>2]"] }
     ],
     invalid: [
         {
@@ -43,6 +48,40 @@ ruleTester.run("no-restricted-syntax", rule, {
                 { message: "Using 'CatchClause' is not allowed.", type: "CatchClause" },
                 { message: "Using 'CallExpression' is not allowed.", type: "CallExpression" }
             ]
+        },
+        {
+            code: "bar;",
+            options: ["Identifier[name=\"bar\"]"],
+            errors: [{ message: "Using 'Identifier[name=\"bar\"]' is not allowed.", type: "Identifier" }]
+        },
+        {
+            code: "bar;",
+            options: ["Identifier", "Identifier[name=\"bar\"]"],
+            errors: [
+                { message: "Using 'Identifier' is not allowed.", type: "Identifier" },
+                { message: "Using 'Identifier[name=\"bar\"]' is not allowed.", type: "Identifier" }
+            ]
+        },
+        {
+            code: "() => {}",
+            parserOptions: { ecmaVersion: 6 },
+            options: ["ArrowFunctionExpression > BlockStatement"],
+            errors: [{ message: "Using 'ArrowFunctionExpression > BlockStatement' is not allowed.", type: "BlockStatement" }]
+        },
+        {
+            code: "({ foo: 1, 'bar': 2 })",
+            options: ["Property > Literal.key"],
+            errors: [{ message: "Using 'Property > Literal.key' is not allowed.", type: "Literal" }]
+        },
+        {
+            code: "A: for (;;) break A;",
+            options: ["BreakStatement[label]"],
+            errors: [{ message: "Using 'BreakStatement[label]' is not allowed.", type: "BreakStatement" }]
+        },
+        {
+            code: "function foo(bar, baz, qux) {}",
+            options: ["FunctionDeclaration[params.length>2]"],
+            errors: [{ message: "Using 'FunctionDeclaration[params.length>2]' is not allowed.", type: "FunctionDeclaration" }]
         }
     ]
 });
diff --git a/tests/lib/util/comment-event-generator.js b/tests/lib/util/comment-event-generator.js
index bbec4bde9..ee4fd031c 100644
--- a/tests/lib/util/comment-event-generator.js
+++ b/tests/lib/util/comment-event-generator.js
@@ -29,7 +29,6 @@ describe("NodeEventGenerator", () => {
 
     it("should generate comment events without duplicate.", () => {
         const emitter = new EventEmitter();
-        let generator = new NodeEventGenerator(emitter);
         const code = "//foo\nvar zzz /*aaa*/ = 777;\n//bar";
         const ast = espree.parse(code, {
             range: true,
@@ -59,6 +58,10 @@ describe("NodeEventGenerator", () => {
             ["Program:exit", ast]
         ];
 
+        expected.forEach(expectedValues => emitter.on(expectedValues[0], () => {}));
+
+        let generator = new NodeEventGenerator(emitter);
+
         emitter.emit = sinon.spy(emitter.emit);
         generator = new CommentEventGenerator(generator, sourceCode);
 
diff --git a/tests/lib/util/node-event-generator.js b/tests/lib/util/node-event-generator.js
index faf63e698..7c875ef18 100644
--- a/tests/lib/util/node-event-generator.js
+++ b/tests/lib/util/node-event-generator.js
@@ -11,6 +11,8 @@
 const assert = require("assert"),
     EventEmitter = require("events").EventEmitter,
     sinon = require("sinon"),
+    espree = require("espree"),
+    estraverse = require("estraverse"),
     EventGeneratorTester = require("../../../lib/testers/event-generator-tester"),
     NodeEventGenerator = require("../../../lib/util/node-event-generator");
 
@@ -18,34 +20,281 @@ const assert = require("assert"),
 // Tests
 //------------------------------------------------------------------------------
 
+const ESPREE_CONFIG = {
+    ecmaVersion: 6,
+    comment: true,
+    tokens: true,
+    range: true,
+    loc: true
+};
+
 describe("NodeEventGenerator", () => {
     EventGeneratorTester.testEventGeneratorInterface(
         new NodeEventGenerator(new EventEmitter())
     );
 
-    let emitter, generator;
+    describe("entering a single AST node", () => {
+        let emitter, generator;
 
-    beforeEach(() => {
-        emitter = new EventEmitter();
-        emitter.emit = sinon.spy(emitter.emit);
-        generator = new NodeEventGenerator(emitter);
-    });
+        beforeEach(() => {
+            emitter = new EventEmitter();
+            ["Foo", "Bar", "Foo > Bar", "Foo:exit"].forEach(selector => emitter.on(selector, () => {}));
+            emitter.emit = sinon.spy(emitter.emit);
+            generator = new NodeEventGenerator(emitter);
+        });
+
+        it("should generate events for entering AST node.", () => {
+            const dummyNode = { type: "Foo", value: 1 };
+
+            generator.enterNode(dummyNode);
+
+            assert(emitter.emit.calledOnce);
+            assert(emitter.emit.calledWith("Foo", dummyNode));
+        });
+
+        it("should generate events for exitting AST node.", () => {
+            const dummyNode = { type: "Foo", value: 1 };
+
+            generator.leaveNode(dummyNode);
+
+            assert(emitter.emit.calledOnce);
+            assert(emitter.emit.calledWith("Foo:exit", dummyNode));
+        });
 
-    it("should generate events for entering AST node.", () => {
-        const dummyNode = { type: "Foo", value: 1 };
+        it("should generate events for AST queries", () => {
+            const dummyNode = { type: "Bar", parent: { type: "Foo" } };
 
-        generator.enterNode(dummyNode);
+            generator.enterNode(dummyNode);
 
-        assert(emitter.emit.calledOnce);
-        assert(emitter.emit.calledWith("Foo", dummyNode));
+            assert(emitter.emit.calledTwice);
+            assert(emitter.emit.calledWith("Foo > Bar", dummyNode));
+        });
     });
 
-    it("should generate events for exitting AST node.", () => {
-        const dummyNode = { type: "Foo", value: 1 };
+    describe("traversing the entire AST", () => {
+
+        /**
+         * Gets a list of emitted types/selectors from the generator, in emission order
+         * @param {ASTNode} ast The AST to traverse
+         * @param {Array<string>|Set<string>} possibleQueries Selectors to detect
+         * @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
+         * array where the first element is a string, and the second element is the emitted AST node.
+         */
+        function getEmissions(ast, possibleQueries) {
+            const emissions = [];
+            const emitter = new EventEmitter();
+
+            possibleQueries.forEach(query => emitter.on(query, () => {}));
+            const generator = new NodeEventGenerator(emitter);
+
+            emitter.emit = (selector, node) => emissions.push([selector, node]);
+
+            estraverse.traverse(ast, {
+                enter(node, parent) {
+                    node.parent = parent;
+                    generator.enterNode(node);
+                },
+                leave(node) {
+                    generator.leaveNode(node);
+                }
+            });
+
+            return emissions;
+        }
+
+        /**
+         * Creates a test case that asserts a particular sequence of generator emissions
+         * @param {string} sourceText The source text that should be parsed and traversed
+         * @param {string[]} possibleQueries A collection of selectors that rules are listening for
+         * @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
+         * generator is expected to produce, in order.
+         * Each element of this list is an array where the first element is a selector (string), and the second is an AST node
+         * This should only include emissions that appear in possibleQueries.
+         * @returns {void}
+         */
+        function assertEmissions(sourceText, possibleQueries, expectedEmissions) {
+            it(possibleQueries.join("; "), () => {
+                const ast = espree.parse(sourceText, ESPREE_CONFIG);
+                const emissions = getEmissions(ast, possibleQueries)
+                    .filter(emission => possibleQueries.indexOf(emission[0]) !== -1);
+
+                assert.deepEqual(emissions, expectedEmissions(ast));
+            });
+        }
+
+        assertEmissions(
+            "foo + bar;",
+            ["Program", "Program:exit", "ExpressionStatement", "ExpressionStatement:exit", "BinaryExpression", "BinaryExpression:exit", "Identifier", "Identifier:exit"],
+            ast => [
+                ["Program", ast], // entering program
+                ["ExpressionStatement", ast.body[0]], // entering 'foo + bar;'
+                ["BinaryExpression", ast.body[0].expression], // entering 'foo + bar'
+                ["Identifier", ast.body[0].expression.left], // entering 'foo'
+                ["Identifier:exit", ast.body[0].expression.left], // exiting 'foo'
+                ["Identifier", ast.body[0].expression.right], // entering 'bar'
+                ["Identifier:exit", ast.body[0].expression.right], // exiting 'bar'
+                ["BinaryExpression:exit", ast.body[0].expression], // exiting 'foo + bar'
+                ["ExpressionStatement:exit", ast.body[0]], // exiting 'foo + bar;'
+                ["Program:exit", ast] // exiting program
+            ]
+        );
+
+        assertEmissions(
+            "foo + 5",
+            [
+                "BinaryExpression > Identifier",
+                "BinaryExpression",
+                "BinaryExpression Literal:exit",
+                "BinaryExpression > Identifier:exit",
+                "BinaryExpression:exit"
+            ],
+            ast => [
+                ["BinaryExpression", ast.body[0].expression], // foo + 5
+                ["BinaryExpression > Identifier", ast.body[0].expression.left], // foo
+                ["BinaryExpression > Identifier:exit", ast.body[0].expression.left], // exiting foo
+                ["BinaryExpression Literal:exit", ast.body[0].expression.right], // exiting 5
+                ["BinaryExpression:exit", ast.body[0].expression] // exiting foo + 5
+            ]
+        );
+
+        assertEmissions(
+            "foo + 5",
+            ["BinaryExpression > *[name='foo']"],
+            ast => [["BinaryExpression > *[name='foo']", ast.body[0].expression.left]] // entering foo
+        );
+
+        assertEmissions(
+            "foo",
+            ["*"],
+            ast => [
+                ["*", ast], // Program
+                ["*", ast.body[0]], // ExpressionStatement
+                ["*", ast.body[0].expression] // Identifier
+            ]
+        );
+
+        assertEmissions(
+            "foo",
+            ["*:not(ExpressionStatement)"],
+            ast => [
+                ["*:not(ExpressionStatement)", ast], // Program
+                ["*:not(ExpressionStatement)", ast.body[0].expression] // Identifier
+            ]
+        );
+
+        assertEmissions(
+            "foo()",
+            ["CallExpression[callee.name='foo']"],
+            ast => [["CallExpression[callee.name='foo']", ast.body[0].expression]] // foo()
+        );
+
+        assertEmissions(
+            "foo()",
+            ["CallExpression[callee.name='bar']"],
+            () => [] // (nothing emitted)
+        );
+
+        assertEmissions(
+            "foo + bar + baz",
+            [":not(*)"],
+            () => [] // (nothing emitted)
+        );
+
+        assertEmissions(
+            "foo + bar + baz",
+            [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])"],
+            ast => [
+                [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.left], // foo
+                [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.right], // bar
+                [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.right] // baz
+            ]
+        );
+
+        assertEmissions(
+            "foo + 5 + 6",
+            ["Identifier, Literal[value=5]"],
+            ast => [
+                ["Identifier, Literal[value=5]", ast.body[0].expression.left.left], // foo
+                ["Identifier, Literal[value=5]", ast.body[0].expression.left.right] // 5
+            ]
+        );
+
+        assertEmissions(
+            "[foo, 5, foo]",
+            ["Identifier + Literal"],
+            ast => [["Identifier + Literal", ast.body[0].expression.elements[1]]]  // 5
+        );
+
+        assertEmissions(
+            "[foo, {}, 5]",
+            ["Identifier + Literal", "Identifier ~ Literal"],
+            ast => [["Identifier ~ Literal", ast.body[0].expression.elements[2]]]  // 5
+        );
+
+        assertEmissions(
+            "foo; bar + baz; qux()",
+            [":expression", ":statement"],
+            ast => [
+                [":statement", ast.body[0]],
+                [":expression", ast.body[0].expression],
+                [":statement", ast.body[1]],
+                [":expression", ast.body[1].expression],
+                [":expression", ast.body[1].expression.left],
+                [":expression", ast.body[1].expression.right],
+                [":statement", ast.body[2]],
+                [":expression", ast.body[2].expression],
+                [":expression", ast.body[2].expression.callee]
+            ]
+        );
+
+        assertEmissions(
+            "foo;",
+            [
+                "*",
+                ":not(*)",
+                "Identifier",
+                "ExpressionStatement > *",
+                "ExpressionStatement > Identifier",
+                "ExpressionStatement > [name='foo']",
+                "Identifier, ReturnStatement",
+                "FooStatement",
+                "[name = 'foo']",
+                "[name='foo']",
+                "Identifier[name='foo']",
+                "[name='foo'][name.length=3]",
+                ":not(Program, ExpressionStatement)",
+                ":not(Program, Identifier) > [name.length=3]"
+            ],
+            ast => [
+                ["*", ast], // Program
+                ["*", ast.body[0]], // ExpressionStatement
+
+                // selectors for the 'foo' identifier, in order of increasing specificity
+                ["*", ast.body[0].expression], // 0 identifiers, 0 pseudoclasses
+                ["ExpressionStatement > *", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
+                ["Identifier", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
+                [":not(Program, ExpressionStatement)", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
+                ["ExpressionStatement > Identifier", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
+                ["Identifier, ReturnStatement", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
+                ["[name = 'foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
+                ["[name='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
+                ["ExpressionStatement > [name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
+                ["Identifier[name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
+                [":not(Program, Identifier) > [name.length=3]", ast.body[0].expression], // 1 attribute, 2 identifiers
+                ["[name='foo'][name.length=3]", ast.body[0].expression] // 2 attributes, 0 identifiers
+            ]
+        );
+    });
 
-        generator.leaveNode(dummyNode);
+    describe("parsing an invalid selector", () => {
+        it("throws a useful error", () => {
+            const emitter = new EventEmitter();
 
-        assert(emitter.emit.calledOnce);
-        assert(emitter.emit.calledWith("Foo:exit", dummyNode));
+            emitter.on("Foo >", () => {});
+            assert.throws(
+                () => new NodeEventGenerator(emitter),
+                /Syntax error in selector "Foo >" at position 5: Expected " ", "!", .*/
+            );
+        });
     });
 });