Repository: eslint/eslint
Pull request #8265 (2017-03-16 22:32:43)

Chore: convert StubModuleResolver in config tests to ES6 class
**What is the purpose of this pull request? (put an "X" next to item)**

[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)**

This converts the internal StubModuleResolver resolver function into an ES6 class.

Unlike the modules described in https://github.com/eslint/eslint/issues/8231, StubModuleResolver is not exposed as part of the public API, so this is not a breaking change.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

eslintbot: LGTM
mention-bot: @not-an-aardvark, thanks for your PR! By analyzing the history of the files in this pull request, we identified @nzakas, @mysticatea 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..ffe763c8b 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
@@ -52,4 +58,3 @@ If you don't want to restrict your code from using any JavaScript features or sy
 * [no-console](no-console.md)
 * [no-debugger](no-debugger.md)
 * [no-restricted-properties](no-restricted-properties.md)
-* [no-restricted-syntax](no-restricted-syntax.md)
diff --git a/docs/rules/no-unused-expressions.md b/docs/rules/no-unused-expressions.md
index 3b4a065e8..480915ee2 100644
--- a/docs/rules/no-unused-expressions.md
+++ b/docs/rules/no-unused-expressions.md
@@ -29,7 +29,8 @@ Sequence expressions (those using a comma, such as `a = 1, b = 2`) are always co
 This rule, in its default state, does not require any arguments. If you would like to enable one or more of the following you may pass an object with the options set as follows:
 
 * `allowShortCircuit` set to `true` will allow you to use short circuit evaluations in your expressions (Default: `false`).
-* `allowTernary` set to `true` will enable you use ternary operators in your expressions similarly to short circuit evaluations (Default: `false`).
+* `allowTernary` set to `true` will enable you to use ternary operators in your expressions similarly to short circuit evaluations (Default: `false`).
+* `allowTaggedTemplates` set to `true` will enable you to use tagged template literals in your expressions (Default: `false`).
 
 These options allow unused expressions *only if all* of the code paths either directly change the state (for example, assignment statement) or could have *side effects* (for example, function call).
 
@@ -56,6 +57,8 @@ a() && function namedFunctionInExpressionContext () {f();}
 
 (function anIncompleteIIFE () {});
 
+injectGlobal`body{ color: red; }`
+
 ```
 
 Note that one or more string expression statements (with or without semi-colons) will only be considered as unused if they are not in the beginning of a script, module, or function (alone and uninterrupted by other statements). Otherwise, they will be treated as part of a "directive prologue", a section potentially usable by JavaScript engines. This includes "strict mode" directives.
@@ -140,3 +143,21 @@ Examples of **correct** code for the `{ "allowShortCircuit": true, "allowTernary
 
 a ? b() || (c = d) : e()
 ```
+
+### allowTaggedTemplates
+
+Examples of **incorrect** code for the `{ "allowTaggedTemplates": true }` option:
+
+```js
+/*eslint no-unused-expressions: ["error", { "allowTaggedTemplates": true }]*/
+
+`some untagged template string`;
+```
+
+Examples of **correct** code for the `{ "allowTaggedTemplates": true }` option:
+
+```js
+/*eslint no-unused-expressions: ["error", { "allowTaggedTemplates": true }]*/
+
+tag`some tagged template string`;
+```
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/formatters/stylish.js b/lib/formatters/stylish.js
index a176d03ab..c19b95d6e 100644
--- a/lib/formatters/stylish.js
+++ b/lib/formatters/stylish.js
@@ -28,7 +28,6 @@ function pluralize(word, count) {
 module.exports = function(results) {
 
     let output = "\n",
-        total = 0,
         errors = 0,
         warnings = 0,
         summaryColor = "yellow";
@@ -40,7 +39,9 @@ module.exports = function(results) {
             return;
         }
 
-        total += messages.length;
+        errors += result.errorCount;
+        warnings += result.warningCount;
+
         output += `${chalk.underline(result.filePath)}\n`;
 
         output += `${table(
@@ -50,10 +51,8 @@ module.exports = function(results) {
                 if (message.fatal || message.severity === 2) {
                     messageType = chalk.red("error");
                     summaryColor = "red";
-                    errors++;
                 } else {
                     messageType = chalk.yellow("warning");
-                    warnings++;
                 }
 
                 return [
@@ -74,6 +73,8 @@ module.exports = function(results) {
         ).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
     });
 
+    const total = errors + warnings;
+
     if (total > 0) {
         output += chalk[summaryColor].bold([
             "\u2716 ", total, pluralize(" problem", total),
diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js
index 35fdfbfa6..8be8a5a49 100644
--- a/lib/rules/no-extra-parens.js
+++ b/lib/rules/no-extra-parens.js
@@ -67,6 +67,7 @@ module.exports = {
         const NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false;
         const EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false;
         const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX;
+        const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
         const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
 
         /**
@@ -394,12 +395,12 @@ module.exports = {
                 report(node.callee);
             }
             if (node.arguments.length === 1) {
-                if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= precedence({ type: "AssignmentExpression" })) {
+                if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                     report(node.arguments[0]);
                 }
             } else {
                 [].forEach.call(node.arguments, arg => {
-                    if (hasExcessParens(arg) && precedence(arg) >= precedence({ type: "AssignmentExpression" })) {
+                    if (hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                         report(arg);
                     }
                 });
@@ -456,7 +457,11 @@ module.exports = {
          * @returns {void}
          */
         function checkSpreadOperator(node) {
-            if (node.argument && hasExcessParens(node.argument)) {
+            const hasExtraParens = precedence(node.argument) >= PRECEDENCE_OF_ASSIGNMENT_EXPR
+                ? hasExcessParens(node.argument)
+                : hasDoubleExcessParens(node.argument);
+
+            if (hasExtraParens) {
                 report(node.argument);
             }
         }
@@ -464,7 +469,7 @@ module.exports = {
         return {
             ArrayExpression(node) {
                 [].forEach.call(node.elements, e => {
-                    if (e && hasExcessParens(e) && precedence(e) >= precedence({ type: "AssignmentExpression" })) {
+                    if (e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                         report(e);
                     }
                 });
@@ -476,7 +481,7 @@ module.exports = {
                 }
 
                 if (node.body.type !== "BlockStatement") {
-                    if (sourceCode.getFirstToken(node.body).value !== "{" && hasExcessParens(node.body) && precedence(node.body) >= precedence({ type: "AssignmentExpression" })) {
+                    if (sourceCode.getFirstToken(node.body).value !== "{" && hasExcessParens(node.body) && precedence(node.body) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                         report(node.body);
                         return;
                     }
@@ -510,11 +515,11 @@ module.exports = {
                     report(node.test);
                 }
 
-                if (hasExcessParens(node.consequent) && precedence(node.consequent) >= precedence({ type: "AssignmentExpression" })) {
+                if (hasExcessParens(node.consequent) && precedence(node.consequent) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                     report(node.consequent);
                 }
 
-                if (hasExcessParens(node.alternate) && precedence(node.alternate) >= precedence({ type: "AssignmentExpression" })) {
+                if (hasExcessParens(node.alternate) && precedence(node.alternate) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                     report(node.alternate);
                 }
             },
@@ -615,7 +620,7 @@ module.exports = {
                 [].forEach.call(node.properties, e => {
                     const v = e.value;
 
-                    if (v && hasExcessParens(v) && precedence(v) >= precedence({ type: "AssignmentExpression" })) {
+                    if (v && hasExcessParens(v) && precedence(v) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
                         report(v);
                     }
                 });
@@ -671,7 +676,7 @@ module.exports = {
 
             VariableDeclarator(node) {
                 if (node.init && hasExcessParens(node.init) &&
-                        precedence(node.init) >= precedence({ type: "AssignmentExpression" }) &&
+                        precedence(node.init) >= PRECEDENCE_OF_ASSIGNMENT_EXPR &&
 
                         // RegExp literal is allowed to have parens (#1589)
                         !(node.init.type === "Literal" && node.init.regex)) {
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/rules/no-unused-expressions.js b/lib/rules/no-unused-expressions.js
index 548e02f46..b4e1074d5 100644
--- a/lib/rules/no-unused-expressions.js
+++ b/lib/rules/no-unused-expressions.js
@@ -25,6 +25,9 @@ module.exports = {
                     },
                     allowTernary: {
                         type: "boolean"
+                    },
+                    allowTaggedTemplates: {
+                        type: "boolean"
                     }
                 },
                 additionalProperties: false
@@ -35,7 +38,8 @@ module.exports = {
     create(context) {
         const config = context.options[0] || {},
             allowShortCircuit = config.allowShortCircuit || false,
-            allowTernary = config.allowTernary || false;
+            allowTernary = config.allowTernary || false,
+            allowTaggedTemplates = config.allowTaggedTemplates || false;
 
         /**
          * @param {ASTNode} node - any node
@@ -95,12 +99,17 @@ module.exports = {
                     return isValidExpression(node.consequent) && isValidExpression(node.alternate);
                 }
             }
+
             if (allowShortCircuit) {
                 if (node.type === "LogicalExpression") {
                     return isValidExpression(node.right);
                 }
             }
 
+            if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") {
+                return true;
+            }
+
             return /^(?:Assignment|Call|New|Update|Yield|Await)Expression$/.test(node.type) ||
                 (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0);
         }
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/config/config-file.js b/tests/lib/config/config-file.js
index 5b6521909..c6ecc09ad 100644
--- a/tests/lib/config/config-file.js
+++ b/tests/lib/config/config-file.js
@@ -123,17 +123,15 @@ function createStubModuleResolver(mapping) {
      * @constructor
      * @private
      */
-    function StubModuleResolver() {}
+    return class StubModuleResolver {
+        resolve(name) { // eslint-disable-line class-methods-use-this
+            if (mapping.hasOwnProperty(name)) {
+                return mapping[name];
+            }
 
-    StubModuleResolver.prototype.resolve = function(name) {
-        if (mapping.hasOwnProperty(name)) {
-            return mapping[name];
+            throw new Error(`Cannot find module '${name}'`);
         }
-
-        throw new Error(`Cannot find module '${name}'`);
     };
-
-    return StubModuleResolver;
 }
 
 //------------------------------------------------------------------------------
diff --git a/tests/lib/formatters/stylish.js b/tests/lib/formatters/stylish.js
index bb63604ec..958693001 100644
--- a/tests/lib/formatters/stylish.js
+++ b/tests/lib/formatters/stylish.js
@@ -59,7 +59,9 @@ describe("formatter:stylish", () => {
     describe("when passed no messages", () => {
         const code = [{
             filePath: "foo.js",
-            messages: []
+            messages: [],
+            errorCount: 0,
+            warningCount: 0
         }];
 
         it("should not return message", () => {
@@ -74,6 +76,8 @@ describe("formatter:stylish", () => {
     describe("when passed a single message", () => {
         const code = [{
             filePath: "foo.js",
+            errorCount: 1,
+            warningCount: 0,
             messages: [{
                 message: "Unexpected foo.",
                 severity: 2,
@@ -93,6 +97,9 @@ describe("formatter:stylish", () => {
 
         it("should return a string in the correct format for warnings", () => {
             code[0].messages[0].severity = 1;
+            code[0].errorCount = 0;
+            code[0].warningCount = 1;
+
             const result = formatter(code);
 
             assert.equal(result, "\nfoo.js\n  5:10  warning  Unexpected foo  foo\n\n\u2716 1 problem (0 errors, 1 warning)\n");
@@ -104,6 +111,8 @@ describe("formatter:stylish", () => {
     describe("when passed a fatal error message", () => {
         const code = [{
             filePath: "foo.js",
+            errorCount: 1,
+            warningCount: 0,
             messages: [{
                 fatal: true,
                 message: "Unexpected foo.",
@@ -125,6 +134,8 @@ describe("formatter:stylish", () => {
     describe("when passed multiple messages", () => {
         const code = [{
             filePath: "foo.js",
+            errorCount: 1,
+            warningCount: 1,
             messages: [{
                 message: "Unexpected foo.",
                 severity: 2,
@@ -152,6 +163,8 @@ describe("formatter:stylish", () => {
     describe("when passed multiple files with 1 message each", () => {
         const code = [{
             filePath: "foo.js",
+            errorCount: 1,
+            warningCount: 0,
             messages: [{
                 message: "Unexpected foo.",
                 severity: 2,
@@ -160,6 +173,8 @@ describe("formatter:stylish", () => {
                 ruleId: "foo"
             }]
         }, {
+            errorCount: 0,
+            warningCount: 1,
             filePath: "bar.js",
             messages: [{
                 message: "Unexpected bar.",
@@ -177,11 +192,39 @@ describe("formatter:stylish", () => {
             assert.equal(chalkStub.yellow.bold.callCount, 0);
             assert.equal(chalkStub.red.bold.callCount, 1);
         });
+
+        it("should add errorCount", () => {
+            code.forEach(c => {
+                c.errorCount = 1;
+                c.warningCount = 0;
+            });
+
+            const result = formatter(code);
+
+            assert.equal(result, "\nfoo.js\n  5:10  error  Unexpected foo  foo\n\nbar.js\n  6:11  warning  Unexpected bar  bar\n\n\u2716 2 problems (2 errors, 0 warnings)\n");
+            assert.equal(chalkStub.yellow.bold.callCount, 0);
+            assert.equal(chalkStub.red.bold.callCount, 1);
+        });
+
+        it("should add warningCount", () => {
+            code.forEach(c => {
+                c.errorCount = 0;
+                c.warningCount = 1;
+            });
+
+            const result = formatter(code);
+
+            assert.equal(result, "\nfoo.js\n  5:10  error  Unexpected foo  foo\n\nbar.js\n  6:11  warning  Unexpected bar  bar\n\n\u2716 2 problems (0 errors, 2 warnings)\n");
+            assert.equal(chalkStub.yellow.bold.callCount, 0);
+            assert.equal(chalkStub.red.bold.callCount, 1);
+        });
     });
 
     describe("when passed one file not found message", () => {
         const code = [{
             filePath: "foo.js",
+            errorCount: 1,
+            warningCount: 0,
             messages: [{
                 fatal: true,
                 message: "Couldn't find foo.js."
diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js
index b1e945e5e..349c14994 100644
--- a/tests/lib/rules/no-extra-parens.js
+++ b/tests/lib/rules/no-extra-parens.js
@@ -380,6 +380,21 @@ ruleTester.run("no-extra-parens", rule, {
                 ecmaFeatures: { experimentalObjectRestSpread: true }
             }
         },
+        {
+            code: "let a = [ ...(b, c) ]",
+            parserOptions: { ecmaVersion: 2015 }
+        },
+        {
+            code: "let a = { ...(b, c) }",
+            parserOptions: {
+                ecmaVersion: 2015,
+                ecmaFeatures: { experimentalObjectRestSpread: true }
+            }
+        },
+        {
+            code: "var [x = (1, foo)] = bar",
+            parserOptions: { ecmaVersion: 2015 }
+        },
         {
             code: "class A extends B {}",
             parserOptions: { ecmaVersion: 2015 }
@@ -891,6 +906,25 @@ ruleTester.run("no-extra-parens", rule, {
                 }
             }
         ),
+        invalid(
+            "let a = [...((b, c))]",
+            "let a = [...(b, c)]",
+            "SequenceExpression",
+            1,
+            { parserOptions: { ecmaVersion: 2015 } }
+        ),
+        invalid(
+            "let a = {...((b, c))}",
+            "let a = {...(b, c)}",
+            "SequenceExpression",
+            1,
+            {
+                parserOptions: {
+                    ecmaVersion: 2015,
+                    ecmaFeatures: { experimentalObjectRestSpread: true }
+                }
+            }
+        ),
         invalid(
             "class A extends (B) {}",
             "class A extends B {}",
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/rules/no-unused-expressions.js b/tests/lib/rules/no-unused-expressions.js
index 57eb39ed5..520705179 100644
--- a/tests/lib/rules/no-unused-expressions.js
+++ b/tests/lib/rules/no-unused-expressions.js
@@ -60,6 +60,16 @@ ruleTester.run("no-unused-expressions", rule, {
             code: "async function foo() { foo ? await bar : await baz; }",
             options: [{ allowTernary: true }],
             parserOptions: { ecmaVersion: 8 }
+        },
+        {
+            code: "tag`tagged template literal`",
+            options: [{ allowTaggedTemplates: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "shouldNotBeAffectedByAllowTemplateTagsOption()",
+            options: [{ allowTaggedTemplates: true }],
+            parserOptions: { ecmaVersion: 6 }
         }
     ],
     invalid: [
@@ -72,6 +82,16 @@ ruleTester.run("no-unused-expressions", rule, {
         { code: "a() || false", errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
         { code: "a || (b = c)", errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
         { code: "a ? b() || (c = d) : e", errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
+        {
+            code: "`untagged template literal`",
+            errors: ["Expected an assignment or function call and instead saw an expression."],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "tag`tagged template literal`",
+            errors: ["Expected an assignment or function call and instead saw an expression."],
+            parserOptions: { ecmaVersion: 6 }
+        },
         { code: "a && b()", options: [{ allowTernary: true }], errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
         { code: "a ? b() : c()", options: [{ allowShortCircuit: true }], errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
         { code: "a || b", options: [{ allowShortCircuit: true }], errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
@@ -85,6 +105,24 @@ ruleTester.run("no-unused-expressions", rule, {
         { code: "function foo() {\"directive one\"; f(); \"directive two\"; }", errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
         { code: "if (0) { \"not a directive\"; f(); }", errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
         { code: "function foo() { var foo = true; \"use strict\"; }", errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
-        { code: "var foo = () => { var foo = true; \"use strict\"; }", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] }
+        { code: "var foo = () => { var foo = true; \"use strict\"; }", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected an assignment or function call and instead saw an expression.", type: "ExpressionStatement" }] },
+        {
+            code: "`untagged template literal`",
+            errors: ["Expected an assignment or function call and instead saw an expression."],
+            options: [{ allowTaggedTemplates: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "`untagged template literal`",
+            errors: ["Expected an assignment or function call and instead saw an expression."],
+            options: [{ allowTaggedTemplates: false }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: "tag`tagged template literal`",
+            errors: ["Expected an assignment or function call and instead saw an expression."],
+            options: [{ allowTaggedTemplates: false }],
+            parserOptions: { ecmaVersion: 6 }
+        }
     ]
 });
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 " ", "!", .*/
+            );
+        });
     });
 });