Repository: eslint/eslint
Pull request #9506 (2017-10-24 00:33:37)

Update: no-restricted-imports - ability to check imported objects
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**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))
[X] 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
[ ] Other, please explain:

**What rule do you want to change?**
https://eslint.org/docs/rules/no-restricted-imports

**Does this change cause the rule to produce more or fewer warnings?**
It adds more granularity to the existing rule.

**How will the change be implemented? (New option, new default behavior, etc.)?**
New option.

**Please provide some example code that this change will affect:**
Please see pull request.  Let me know if you need anything extra.

**What does the rule currently do for this code?**
The rule does not currently support this additional parameter.

**What will the rule do after it's changed?**
After my change, you will be able to restrict certain objects from an import (instead of all objects). 

This scenario will pass:
```js
/*eslint no-restricted-imports: ["error", { paths: [{ name: "foo", objectNames: ["DisallowedObject"] }] }]*/

import AllowedObject from "foo"
```

This scenario will fail:
```js
/*eslint no-restricted-imports: ["error", { paths: [{
    name: "foo",
    objectNames: ["DisallowedObject"],
    message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
}]}]*/

import DisallowedObject from "foo";
```

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**
You will notice that there are two commits in this Pull Request.  
- The first is some simple refactoring for readability of code that I had to read multiple times in order to understand.
- The second commit is the real change (I will be shortly adding comments to the Pull Request explaining my choices).

**Is there anything you'd like reviewers to focus on?**

What do you think about the naming of the new field, ```objectName```.  Are there any better names you can think of?

~~Would it be better to change this new field to an array of ```objectNames``` instead (so that a user would be able to restrict multiple objects with one rule?  I'm leaning towards yes.~~
I have implemented this.

eslintbot: Thanks for the pull request, @brgibson! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](https://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](https://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](https://eslint.org/docs/developer-guide/contributing/pull-requests).)
jsf-clabot: [![CLA assistant check](https://cla.js.foundation/pull/badge/not_signed)](https://cla.js.foundation/eslint/eslint?pullRequest=9506) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.js.foundation/eslint/eslint?pullRequest=9506) before we can accept your contribution.<br/>
eslintbot: LGTM
eslintbot: LGTM
eslintbot: LGTM
eslintbot: LGTM
eslintbot: LGTM
brgibson: > "In general, I'm OK with adding this enhancement. I want to know how you want to handle importing * if there's a restriction on the specific path. Since * will import everything, and you wanted to restrict at least one property, most likely it should be flagged."

Good catch.  It is now flagging when * is used.
eslintbot: LGTM
brgibson: I'm still working on converting the patterns (config option) to accept strings or objects.  As part of this, I'm going to try and make a few less branches in the code.
brgibson: I think I can see why they kept the patterns separate.  Since the patterns all work together you can't break out the array in the same way [.gitignore spec](https://git-scm.com/docs/gitignore).  They aren't separate regex patterns, instead it is like writing a .gitignore file full of rules that interplay and override certain paths set by another rule in the pattern.
brgibson: @ilyavolodin I believe I have made all of the recommended changes.  
- flagging '*' import statements
- adding more unit tests

I did not end up converting the implementation of the gitignore "patterns" type.

Would you take another look?  Thanks!

Also, I'm not sure why the commit-message validation is failing.  It was passing earlier and all I have done is amend my commit.
 
brgibson: @ilyavolodin is it a problem that the commit-message check is failing?  Last time I believe I fixed this failure by squashing my commits.  I'm happy to do that again, if you want.
not-an-aardvark: The issue is that the title of this PR doesn't follow the commit message guidelines. When a PR has more than two commits, the title is the default commit message in the GitHub UI when we try to merge it. In those cases, we want to make sure that the title follows the commit guidelines so that we don't accidentally merge the PR with an invalid commit message.
brgibson: Thanks for the fix @not-an-aardvark. That makes sense.
brgibson: @ilyavolodin do you have any more suggestions.  How would I go about getting this on other devs radar.  I don't want to lose any momentum on this.  It will be a valuable change for my team :)
brgibson: Latest push was a squash and rebase from the latest master.
not-an-aardvark: I'm confused by this proposal. What does it mean to "restrict certain objects from an import"? `import AllowedObject from "foo"` and `import DisallowedObject from "foo";` are doing the same thing -- they just give the variable a different name.
brgibson: @not-an-aardvark Good catch, thanks! 

The main use case that I am trying to support is `import { DisallowedObject } from "foo";`

I was forgetting that when importing a default export it behaves as an alias.  I have added logic to check for this in my latest commit.

brgibson: @not-an-aardvark I believe I have made fixes for all of your great feedback.  Let me know if you see anything else!
brgibson: @ilyavolodin, Now that @not-an-aardvark has signed off, would you be able to give this another kick to get additional reviewers?

Thanks!
platinumazure: Thanks, now I see we were still in the valid section. Sorry about that. Seems we shouldn't allow errors to be specified in valid tests in RuleTester, but that's a separate issue.

I'll give a full review tomorrow and will probably approve then. Thanks for your patience and for being so quick to respond to feedback!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e03df222..2df883289 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,39 @@
+v4.11.0 - November 10, 2017
+
+* d4557a6 Docs: disallow use of the comma operator using no-restricted-syntax (#9585) (薛定谔的猫)
+* d602f9e Upgrade: espree v3.5.2 (#9611) (Kai Cataldo)
+* 4def876 Chore: avoid handling rules instances in config-validator (#9364) (Teddy Katz)
+* fe5ac7e Chore: fix incorrect comment in safe-emitter.js (#9605) (Teddy Katz)
+* 6672fae Docs: Fixed a typo on lines-between-class-members doc (#9603) (Moinul Hossain)
+* 980ecd3 Chore: Update copyright and license info (#9599) (薛定谔的猫)
+* cc2c7c9 Build: use Node 8 in appveyor (#9595) (薛定谔的猫)
+* 2542f04 Docs: Add missing options for `lines-around-comment` (#9589) (Clément Fiorio)
+* b6a7490 Build: ensure fuzzer tests get run with `npm test` (#9590) (Teddy Katz)
+* 1073bc5 Build: remove shelljs-nodecli (refs #9533) (#9588) (Teddy Katz)
+* 7e3bf6a Fix: edge-cases of semi-style (#9560) (Toru Nagashima)
+* e5a37ce Fix: object-curly-newline for flow code (#9458) (Tiddo Langerak)
+* 9064b9c Chore: add equalTokens in ast-utils. (#9500) (薛定谔的猫)
+* b7c5b19 Fix: Correct [object Object] output of error.data. (#9561) (Jonathan Pool)
+* 51c8cf0 Docs: Disambiguate definition of Update tag (#9584) (Jonathan Pool)
+* afc3c75 Docs: clarify what eslint-config-eslint is (#9582) (Teddy Katz)
+* aedae9d Docs: fix spelling in valid-typeof example (#9574) (Maksim Degtyarev)
+* 4c5aaf3 Docs: Fix typo in no-underscore-dangle rule (#9567) (Fabien Lucas)
+* 3623600 Chore: upgrade ajv@5.3.0 (#9557) (薛定谔的猫)
+* 1b606cd Chore: Remove an indirect dependency on jsonify (#9444) (Rouven Weßling)
+* 4d7d7ab Update: Resolve npm installed formatters (#5900) (#9464) (Tom Erik Støwer)
+* accc490 Fix: Files with no failures get "passing" testcase (#9547) (Samuel Levy)
+* ab0f66d Docs: Add examples to better show rule coverage. (#9548) (Jonathan Pool)
+* 88d2303 Chore: Add object-property-newline tests to increase coverage. (#9553) (Jonathan Pool)
+* 7f37b1c Build: test Node 9 on Travis (#9556) (Teddy Katz)
+* acccfbd Docs: Minor rephrase in `no-invalid-this`. (#9542) (Francisc)
+* 8f9c0fe Docs: improve id-match usage advice (#9544) (Teddy Katz)
+* a9606a3 Fix: invalid tests with super (fixes #9539) (#9545) (Teddy Katz)
+* 8e1a095 Chore: enable a modified version of multiline-comment-style on codebase (#9452) (Teddy Katz)
+* cb60285 Chore: remove commented test for HTML formatter (#9532) (Teddy Katz)
+* 06b491e Docs: fix duplicate entries in changelog (#9530) (Teddy Katz)
+* 2224733 Chore: use eslint-plugin-rulesdir instead of --rulesdir for self-linting (#9164) (Teddy Katz)
+* 9cf4ebe Docs: add .md to link(for github users) (#9529) (薛定谔的猫)
+
 v4.10.0 - October 27, 2017
 
 * bb6e60a Fix: Improve the doc for no-restricted-modules rule (fixes #9437) (#9495) (vibss2397)
diff --git a/Makefile.js b/Makefile.js
index 544ee8fc6..02363aa66 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -20,7 +20,6 @@ const lodash = require("lodash"),
     fs = require("fs"),
     glob = require("glob"),
     markdownlint = require("markdownlint"),
-    nodeCLI = require("shelljs-nodecli"),
     os = require("os"),
     path = require("path"),
     semver = require("semver"),
@@ -103,7 +102,7 @@ function getTestFilePatterns() {
             initialValue.push(`"tests/lib/${currentValues}/**/*.js"`);
         }
         return initialValue;
-    }, ["tests/lib/rules/**/*.js", "tests/lib/*.js", "tests/bin/**/*.js", "tests/tools/**/*.js"]).join(" ");
+    }, ["\"tests/lib/rules/**/*.js\"", "\"tests/lib/*.js\"", "\"tests/bin/**/*.js\"", "\"tests/tools/**/*.js\""]).join(" ");
 }
 
 /**
@@ -468,6 +467,15 @@ function getFormatterResults() {
     }, { formatterResults: {} });
 }
 
+/**
+ * Gets a path to an executable in node_modules/.bin
+ * @param {string} command The executable name
+ * @returns {string} The executable path
+ */
+function getBinFile(command) {
+    return path.join("node_modules", ".bin", command);
+}
+
 //------------------------------------------------------------------------------
 // Tasks
 //------------------------------------------------------------------------------
@@ -560,21 +568,20 @@ target.test = function() {
     let errors = 0,
         lastReturn;
 
-    // exec(ISTANBUL + " cover " + MOCHA + "-- -c " + TEST_FILES);
-    lastReturn = nodeCLI.exec("istanbul", "cover", MOCHA, `-- -R progress -t ${MOCHA_TIMEOUT}`, "-c", TEST_FILES);
+    lastReturn = exec(`${getBinFile("istanbul")} cover ${MOCHA} -- -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`);
     if (lastReturn.code !== 0) {
         errors++;
     }
 
-    // exec(ISTANBUL + "check-coverage --statement 99 --branch 98 --function 99 --lines 99");
-    lastReturn = nodeCLI.exec("istanbul", "check-coverage", "--statement 99 --branch 98 --function 99 --lines 99");
+    lastReturn = exec(`${getBinFile("istanbul")} check-coverage --statement 99 --branch 98 --function 99 --lines 99`);
+
     if (lastReturn.code !== 0) {
         errors++;
     }
 
     target.browserify();
 
-    lastReturn = nodeCLI.exec("karma", "start karma.conf.js");
+    lastReturn = exec(`${getBinFile("karma")} start karma.conf.js`);
     if (lastReturn.code !== 0) {
         errors++;
     }
@@ -588,7 +595,7 @@ target.test = function() {
 
 target.docs = function() {
     echo("Generating documentation");
-    nodeCLI.exec("jsdoc", "-d jsdoc lib");
+    exec(`${getBinFile("jsdoc")} -d jsdoc lib`);
     echo("Documentation has been output to /jsdoc");
 };
 
@@ -799,10 +806,10 @@ target.browserify = function() {
     generateRulesIndex(TEMP_DIR);
 
     // 5. browserify the temp directory
-    nodeCLI.exec("browserify", "-x espree", `${TEMP_DIR}linter.js`, "-o", `${BUILD_DIR}eslint.js`, "-s eslint", "--global-transform [ babelify --presets [ es2015 ] ]");
+    exec(`${getBinFile("browserify")} -x espree ${TEMP_DIR}linter.js -o ${BUILD_DIR}eslint.js -s eslint --global-transform [ babelify --presets [ es2015 ] ]`);
 
     // 6. Browserify espree
-    nodeCLI.exec("browserify", "-r espree", "-o", `${TEMP_DIR}espree.js`);
+    exec(`${getBinFile("browserify")} -r espree -o ${TEMP_DIR}espree.js`);
 
     // 7. Concatenate Babel polyfill, Espree, and ESLint files together
     cat("./node_modules/babel-polyfill/dist/polyfill.js", `${TEMP_DIR}espree.js`, `${BUILD_DIR}eslint.js`).to(`${BUILD_DIR}eslint.js`);
diff --git a/README.md b/README.md
index 9da7d9cce..3b7d6aca4 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,7 @@ These folks keep the project moving and are resources for help.
 * Alberto Rodríguez ([@alberto](https://github.com/alberto))
 * Kai Cataldo ([@kaicataldo](https://github.com/kaicataldo))
 * Teddy Katz ([@not-an-aardvark](https://github.com/not-an-aardvark))
+* Kevin Partington ([@platinumazure](https://github.com/platinumazure))
 
 ### Development Team
 
@@ -130,7 +131,6 @@ These folks keep the project moving and are resources for help.
 * Henry Zhu ([@hzoo](https://github.com/hzoo))
 * Marat Dulin ([@mdevils](https://github.com/mdevils))
 * Alexej Yaroshevich ([@zxqfox](https://github.com/zxqfox))
-* Kevin Partington ([@platinumazure](https://github.com/platinumazure))
 * Vitor Balocco ([@vitorbal](https://github.com/vitorbal))
 * James Henry ([@JamesHenry](https://github.com/JamesHenry))
 * Reyad Attiyat ([@soda0289](https://github.com/soda0289))
diff --git a/appveyor.yml b/appveyor.yml
index e424a345b..966263e29 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,7 +7,7 @@ version: "{build}"
 # What combinations to test
 environment:
   matrix:
-    - nodejs_version: 4
+    - nodejs_version: 8
 
 branches:
   only:
diff --git a/docs/rules/lines-around-comment.md b/docs/rules/lines-around-comment.md
index ae8ef4de7..61af59698 100644
--- a/docs/rules/lines-around-comment.md
+++ b/docs/rules/lines-around-comment.md
@@ -21,6 +21,8 @@ This rule has an object option:
 * `"allowObjectEnd": true` allows comments to appear at the end of object literals
 * `"allowArrayStart": true` allows comments to appear at the start of array literals
 * `"allowArrayEnd": true` allows comments to appear at the end of array literals
+* `"allowClassStart": true` allows comments to appear at the start of classes
+* `"allowClassEnd": true` allows comments to appear at the end of classes
 * `"applyDefaultIgnorePatterns"` enables or disables the default comment patterns to be ignored by the rule
 * `"ignorePattern"` custom patterns to be ignored by the rule
 
diff --git a/docs/rules/lines-between-class-members.md b/docs/rules/lines-between-class-members.md
index c17b7a1aa..5196b6ad5 100644
--- a/docs/rules/lines-between-class-members.md
+++ b/docs/rules/lines-between-class-members.md
@@ -102,6 +102,6 @@ If you don't want to enforce empty lines between class members, you can disable
 ## Related Rules
 
 * [padded-blocks](padded-blocks.md)
-* [padding-line-between-statement](padding-line-between-statement.md)
+* [padding-line-between-statements](padding-line-between-statements.md)
 * [requirePaddingNewLinesAfterBlocks](http://jscs.info/rule/requirePaddingNewLinesAfterBlocks)
 * [disallowPaddingNewLinesAfterBlocks](http://jscs.info/rule/disallowPaddingNewLinesAfterBlocks)
diff --git a/docs/rules/no-restricted-imports.md b/docs/rules/no-restricted-imports.md
index 420ce4e58..66b8095a0 100644
--- a/docs/rules/no-restricted-imports.md
+++ b/docs/rules/no-restricted-imports.md
@@ -55,6 +55,18 @@ or like this:
 }]
 ```
 
+or like this if you need to restrict only certain imports from a module:
+
+```json
+"no-restricted-imports": ["error", {
+  "paths": [{
+    "name": "import-foo",
+    "importNames": ["Bar"],
+    "message": "Please use Bar from /import-bar/baz/ instead."
+  }]
+}]
+```
+
 The custom message will be appended to the default error message. Please note that you may not specify custom error messages for restricted patterns as a particular import may match more than one pattern.
 
 To restrict the use of all Node.js core imports (via https://github.com/nodejs/node/tree/master/lib):
@@ -87,6 +99,36 @@ import cluster from 'cluster';
 import pick from 'lodash/pick';
 ```
 
+```js
+/*eslint no-restricted-imports: ["error", { paths: [{
+    name: "foo",
+    importNames: ["default"],
+    message: "Please use the default import from '/bar/baz/' instead."
+}]}]*/
+
+import DisallowedObject from "foo";
+```
+
+```js
+/*eslint no-restricted-imports: ["error", { paths: [{
+    name: "foo",
+    importNames: ["DisallowedObject"],
+    message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
+}]}]*/
+
+import { DisallowedObject as AllowedObject } from "foo";
+```
+
+```js
+/*eslint no-restricted-imports: ["error", { paths: [{
+    name: "foo",
+    importNames: ["DisallowedObject"],
+    message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
+}]}]*/
+
+import * as Foo from "foo";
+```
+
 Examples of **correct** code for this rule:
 
 ```js
@@ -102,6 +144,22 @@ import crypto from 'crypto';
 import eslint from 'eslint';
 ```
 
+```js
+/*eslint no-restricted-imports: ["error", { paths: [{ name: "foo", importNames: ["DisallowedObject"] }] }]*/
+
+import DisallowedObject from "foo"
+```
+
+```js
+/*eslint no-restricted-imports: ["error", { paths: [{
+    name: "foo",
+    importNames: ["DisallowedObject"],
+    message: "Please import 'DisallowedObject' from '/bar/baz/' instead."
+}]}]*/
+
+import { AllowedObject as DisallowedObject } from "foo";
+```
+
 ## When Not To Use It
 
 Don't use this rule or don't include a module in the list for this rule if you want to be able to import a module in your project without an ESLint error or warning.
diff --git a/docs/rules/no-sequences.md b/docs/rules/no-sequences.md
index 1d05e8255..f1f4afea0 100644
--- a/docs/rules/no-sequences.md
+++ b/docs/rules/no-sequences.md
@@ -66,3 +66,12 @@ while ((val = foo(), val < 42));
 ## When Not To Use It
 
 Disable this rule if sequence expressions with the comma operator are acceptable.
+Another case is where you might want to report all usages of the comma operator, even if they are wrapped in parentheses or in a for loop. You can achieve this using rule `no-restricted-syntax`:
+
+```js
+{
+    "rules": {
+        "no-restricted-syntax": ["error", "SequenceExpression"]
+    }
+}
+```
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index 39169dc42..55450fd63 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -409,9 +409,13 @@ class CLIEngine {
             });
         }
 
-        Object.keys(this.options.rules || {}).forEach(name => {
-            validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
-        });
+        if (this.options.rules && Object.keys(this.options.rules).length) {
+            const loadedRules = this.linter.getRules();
+
+            Object.keys(this.options.rules).forEach(name => {
+                validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI");
+            });
+        }
 
         this.config = new Config(this.options, this.linter);
     }
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index 8cefd437d..c5ff073cf 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -501,8 +501,10 @@ function loadFromDisk(resolvedPath, configContext) {
             }
         }
 
+        const ruleMap = configContext.linterContext.getRules();
+
         // validate the configuration before continuing
-        validator.validate(config, resolvedPath.configFullName, configContext.linterContext.rules, configContext.linterContext.environments);
+        validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments);
 
         /*
          * If an `extends` property is defined, it represents a configuration file to use as
diff --git a/lib/config/config-validator.js b/lib/config/config-validator.js
index f2307dc63..3d98b5104 100644
--- a/lib/config/config-validator.js
+++ b/lib/config/config-validator.js
@@ -14,9 +14,7 @@ const ajv = require("../util/ajv"),
     configSchema = require("../../conf/config-schema.js"),
     util = require("util");
 
-const validators = {
-    rules: Object.create(null)
-};
+const ruleValidators = new WeakMap();
 
 //------------------------------------------------------------------------------
 // Private
@@ -25,13 +23,11 @@ let validateSchema;
 
 /**
  * Gets a complete options schema for a rule.
- * @param {string} id The rule's unique name.
- * @param {Rules} rulesContext Rule context
+ * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
  * @returns {Object} JSON Schema for the rule's options.
  */
-function getRuleOptionsSchema(id, rulesContext) {
-    const rule = rulesContext.get(id),
-        schema = rule && rule.schema || rule && rule.meta && rule.meta.schema;
+function getRuleOptionsSchema(rule) {
+    const schema = rule.schema || rule.meta && rule.meta.schema;
 
     // Given a tuple of schemas, insert warning level at the beginning
     if (Array.isArray(schema)) {
@@ -72,19 +68,20 @@ function validateRuleSeverity(options) {
 
 /**
  * Validates the non-severity options passed to a rule, based on its schema.
- * @param {string} id The rule's unique name
+ * @param {{create: Function}} rule The rule to validate
  * @param {array} localOptions The options for the rule, excluding severity
- * @param {Rules} rulesContext Rule context
  * @returns {void}
  */
-function validateRuleSchema(id, localOptions, rulesContext) {
-    const schema = getRuleOptionsSchema(id, rulesContext);
+function validateRuleSchema(rule, localOptions) {
+    if (!ruleValidators.has(rule)) {
+        const schema = getRuleOptionsSchema(rule);
 
-    if (!validators.rules[id] && schema) {
-        validators.rules[id] = ajv.compile(schema);
+        if (schema) {
+            ruleValidators.set(rule, ajv.compile(schema));
+        }
     }
 
-    const validateRule = validators.rules[id];
+    const validateRule = ruleValidators.get(rule);
 
     if (validateRule) {
         validateRule(localOptions);
@@ -98,21 +95,24 @@ function validateRuleSchema(id, localOptions, rulesContext) {
 
 /**
  * Validates a rule's options against its schema.
- * @param {string} id The rule's unique name.
+ * @param {{create: Function}|null} rule The rule that the config is being validated for
+ * @param {string} ruleId The rule's unique name.
  * @param {array|number} options The given options for the rule.
  * @param {string} source The name of the configuration source to report in any errors.
- * @param {Rules} rulesContext Rule context
  * @returns {void}
  */
-function validateRuleOptions(id, options, source, rulesContext) {
+function validateRuleOptions(rule, ruleId, options, source) {
+    if (!rule) {
+        return;
+    }
     try {
         const severity = validateRuleSeverity(options);
 
         if (severity !== 0 && !(typeof severity === "string" && severity.toLowerCase() === "off")) {
-            validateRuleSchema(id, Array.isArray(options) ? options.slice(1) : [], rulesContext);
+            validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
         }
     } catch (err) {
-        throw new Error(`${source}:\n\tConfiguration for rule "${id}" is invalid:\n${err.message}`);
+        throw new Error(`${source}:\n\tConfiguration for rule "${ruleId}" is invalid:\n${err.message}`);
     }
 }
 
@@ -143,16 +143,16 @@ function validateEnvironment(environment, source, envContext) {
  * Validates a rules config object
  * @param {Object} rulesConfig The rules config object to validate.
  * @param {string} source The name of the configuration source to report in any errors.
- * @param {Rules} rulesContext Rule context
+ * @param {function(string): {create: Function}} ruleMapper A mapper function from strings to loaded rules
  * @returns {void}
  */
-function validateRules(rulesConfig, source, rulesContext) {
+function validateRules(rulesConfig, source, ruleMapper) {
     if (!rulesConfig) {
         return;
     }
 
     Object.keys(rulesConfig).forEach(id => {
-        validateRuleOptions(id, rulesConfig[id], source, rulesContext);
+        validateRuleOptions(ruleMapper(id), id, rulesConfig[id], source);
     });
 }
 
@@ -223,13 +223,13 @@ function validateConfigSchema(config, source) {
  * Validates an entire config object.
  * @param {Object} config The config object to validate.
  * @param {string} source The name of the configuration source to report in any errors.
- * @param {Rules} rulesContext The rules context
+ * @param {function(string): {create: Function}} ruleMapper A mapper function from rule IDs to defined rules
  * @param {Environments} envContext The env context
  * @returns {void}
  */
-function validate(config, source, rulesContext, envContext) {
+function validate(config, source, ruleMapper, envContext) {
     validateConfigSchema(config, source);
-    validateRules(config.rules, source, rulesContext);
+    validateRules(config.rules, source, ruleMapper);
     validateEnvironment(config.env, source, envContext);
 }
 
diff --git a/lib/linter.js b/lib/linter.js
index 128798424..35ecf7c75 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -278,7 +278,7 @@ function createDisableDirectives(type, loc, value) {
  * @param {string} filename The file being checked.
  * @param {ASTNode} ast The top node of the AST.
  * @param {Object} config The existing configuration data.
- * @param {Linter} linterContext Linter context object
+ * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
  * @returns {{
  *      config: Object,
  *      problems: Problem[],
@@ -291,7 +291,7 @@ function createDisableDirectives(type, loc, value) {
  * }} Modified config object, along with any problems encountered
  * while parsing config comments
  */
-function modifyConfigsFromComments(filename, ast, config, linterContext) {
+function modifyConfigsFromComments(filename, ast, config, ruleMapper) {
 
     const commentConfig = {
         exported: {},
@@ -337,7 +337,7 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
                             Object.keys(parseResult.config).forEach(name => {
                                 const ruleValue = parseResult.config[name];
 
-                                validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
+                                validator.validateRuleOptions(ruleMapper(name), name, ruleValue, `${filename} line ${comment.loc.start.line}`);
                                 commentRules[name] = ruleValue;
                             });
                         } else {
@@ -806,7 +806,7 @@ module.exports = class Linter {
 
         // parse global comments and modify config
         if (allowInlineConfig !== false) {
-            const modifyConfigResult = modifyConfigsFromComments(filename, sourceCode.ast, config, this);
+            const modifyConfigResult = modifyConfigsFromComments(filename, sourceCode.ast, config, ruleId => this.rules.get(ruleId));
 
             config = modifyConfigResult.config;
             modifyConfigResult.problems.forEach(problem => problems.push(problem));
diff --git a/lib/rules/no-restricted-imports.js b/lib/rules/no-restricted-imports.js
index d46b098ac..eb477b4be 100644
--- a/lib/rules/no-restricted-imports.js
+++ b/lib/rules/no-restricted-imports.js
@@ -8,8 +8,8 @@
 // Helpers
 //------------------------------------------------------------------------------
 
-const DEFAULT_MESSAGE_TEMPLATE = "'{{importName}}' import is restricted from being used.";
-const CUSTOM_MESSAGE_TEMPLATE = "'{{importName}}' import is restricted from being used. {{customMessage}}";
+const DEFAULT_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used.";
+const CUSTOM_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used. {{customMessage}}";
 
 //------------------------------------------------------------------------------
 // Rule Definition
@@ -35,6 +35,12 @@ const arrayOfStringsOrObjects = {
                     message: {
                         type: "string",
                         minLength: 1
+                    },
+                    importNames: {
+                        type: "array",
+                        items: {
+                            type: "string"
+                        }
                     }
                 },
                 additionalProperties: false,
@@ -81,11 +87,14 @@ module.exports = {
         const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
         const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
 
-        const restrictedPathMessages = restrictedPaths.reduce((memo, importName) => {
-            if (typeof importName === "string") {
-                memo[importName] = null;
+        const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
+            if (typeof importSource === "string") {
+                memo[importSource] = { message: null };
             } else {
-                memo[importName.name] = importName.message;
+                memo[importSource.name] = {
+                    message: importSource.message,
+                    importNames: importSource.importNames
+                };
             }
             return memo;
         }, {});
@@ -95,7 +104,16 @@ module.exports = {
             return {};
         }
 
-        const ig = ignore().add(restrictedPatterns);
+        const restrictedPatternsMatcher = ignore().add(restrictedPatterns);
+
+        /**
+         * Checks to see if "*" is being used to import everything.
+         * @param {Set.<string>} importNames - Set of import names that are being imported
+         * @returns {boolean} whether everything is imported or not
+         */
+        function isEverythingImported(importNames) {
+            return importNames.has("*");
+        }
 
         /**
          * Report a restricted path.
@@ -104,8 +122,8 @@ module.exports = {
          * @private
          */
         function reportPath(node) {
-            const importName = node.source.value.trim();
-            const customMessage = restrictedPathMessages[importName];
+            const importSource = node.source.value.trim();
+            const customMessage = restrictedPathMessages[importSource] && restrictedPathMessages[importSource].message;
             const message = customMessage
                 ? CUSTOM_MESSAGE_TEMPLATE
                 : DEFAULT_MESSAGE_TEMPLATE;
@@ -114,39 +132,130 @@ module.exports = {
                 node,
                 message,
                 data: {
-                    importName,
+                    importSource,
                     customMessage
                 }
             });
         }
 
         /**
-         * Check if the given name is a restricted path name.
-         * @param {string} name name of a variable
+         * Report a restricted path specifically for patterns.
+         * @param {node} node - representing the restricted path reference
+         * @returns {void}
+         * @private
+         */
+        function reportPathForPatterns(node) {
+            const importSource = node.source.value.trim();
+
+            context.report({
+                node,
+                message: "'{{importSource}}' import is restricted from being used by a pattern.",
+                data: {
+                    importSource
+                }
+            });
+        }
+
+        /**
+         * Report a restricted path specifically when using the '*' import.
+         * @param {string} importSource - path of the import
+         * @param {node} node - representing the restricted path reference
+         * @returns {void}
+         * @private
+         */
+        function reportPathForEverythingImported(importSource, node) {
+            const importNames = restrictedPathMessages[importSource].importNames;
+
+            context.report({
+                node,
+                message: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
+                data: {
+                    importSource,
+                    importNames
+                }
+            });
+        }
+
+        /**
+         * Check if the given importSource is restricted because '*' is being imported.
+         * @param {string} importSource - path of the import
+         * @param {Set.<string>} importNames - Set of import names that are being imported
+         * @returns {boolean} whether the path is restricted
+         * @private
+         */
+        function isRestrictedForEverythingImported(importSource, importNames) {
+            return Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource) &&
+                restrictedPathMessages[importSource].importNames &&
+                isEverythingImported(importNames);
+        }
+
+        /**
+         * Check if the given importNames are restricted given a list of restrictedImportNames.
+         * @param {Set.<string>} importNames - Set of import names that are being imported
+         * @param {[string]} restrictedImportNames - array of import names that are restricted for this import
+         * @returns {boolean} whether the objectName is restricted
+         * @private
+         */
+        function isRestrictedObject(importNames, restrictedImportNames) {
+            return restrictedImportNames.some(restrictedObjectName => (
+                importNames.has(restrictedObjectName)
+            ));
+        }
+
+        /**
+         * Check if the given importSource is a restricted path.
+         * @param {string} importSource - path of the import
+         * @param {Set.<string>} importNames - Set of import names that are being imported
          * @returns {boolean} whether the variable is a restricted path or not
          * @private
          */
-        function isRestrictedPath(name) {
-            return Object.prototype.hasOwnProperty.call(restrictedPathMessages, name);
+        function isRestrictedPath(importSource, importNames) {
+            let isRestricted = false;
+
+            if (Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
+                if (restrictedPathMessages[importSource].importNames) {
+                    isRestricted = isRestrictedObject(importNames, restrictedPathMessages[importSource].importNames);
+                } else {
+                    isRestricted = true;
+                }
+            }
+
+            return isRestricted;
+        }
+
+        /**
+         * Check if the given importSource is restricted by a pattern.
+         * @param {string} importSource - path of the import
+         * @returns {boolean} whether the variable is a restricted pattern or not
+         * @private
+         */
+        function isRestrictedPattern(importSource) {
+            return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource);
         }
 
         return {
             ImportDeclaration(node) {
-                if (node && node.source && node.source.value) {
-                    const importName = node.source.value.trim();
-
-                    if (isRestrictedPath(importName)) {
-                        reportPath(node);
-                    }
-                    if (restrictedPatterns.length > 0 && ig.ignores(importName)) {
-                        context.report({
-                            node,
-                            message: "'{{importName}}' import is restricted from being used by a pattern.",
-                            data: {
-                                importName
-                            }
-                        });
+                const importSource = node.source.value.trim();
+                const importNames = node.specifiers.reduce((set, specifier) => {
+                    if (specifier.type === "ImportDefaultSpecifier") {
+                        set.add("default");
+                    } else if (specifier.type === "ImportNamespaceSpecifier") {
+                        set.add("*");
+                    } else {
+                        set.add(specifier.imported.name);
                     }
+                    return set;
+                }, new Set());
+
+                if (isRestrictedForEverythingImported(importSource, importNames)) {
+                    reportPathForEverythingImported(importSource, node);
+                }
+
+                if (isRestrictedPath(importSource, importNames)) {
+                    reportPath(node);
+                }
+                if (isRestrictedPattern(importSource)) {
+                    reportPathForPatterns(node);
                 }
             }
         };
diff --git a/lib/rules/semi-style.js b/lib/rules/semi-style.js
index 97fcc3ac8..41fb39246 100644
--- a/lib/rules/semi-style.js
+++ b/lib/rules/semi-style.js
@@ -18,13 +18,51 @@ const astUtils = require("../ast-utils");
 const SELECTOR = `:matches(${
     [
         "BreakStatement", "ContinueStatement", "DebuggerStatement",
-        "DoWhileStatement", "EmptyStatement", "ExportAllDeclaration",
+        "DoWhileStatement", "ExportAllDeclaration",
         "ExportDefaultDeclaration", "ExportNamedDeclaration",
         "ExpressionStatement", "ImportDeclaration", "ReturnStatement",
         "ThrowStatement", "VariableDeclaration"
     ].join(",")
 })`;
 
+/**
+ * Get the child node list of a given node.
+ * This returns `Program#body`, `BlockStatement#body`, or `SwitchCase#consequent`.
+ * This is used to check whether a node is the first/last child.
+ * @param {Node} node A node to get child node list.
+ * @returns {Node[]|null} The child node list.
+ */
+function getChildren(node) {
+    const t = node.type;
+
+    if (t === "BlockStatement" || t === "Program") {
+        return node.body;
+    }
+    if (t === "SwitchCase") {
+        return node.consequent;
+    }
+    return null;
+}
+
+/**
+ * Check whether a given node is the last statement in the parent block.
+ * @param {Node} node A node to check.
+ * @returns {boolean} `true` if the node is the last statement in the parent block.
+ */
+function isLastChild(node) {
+    const t = node.parent.type;
+
+    if (t === "IfStatement" && node.parent.consequent === node && node.parent.alternate) { // before `else` keyword.
+        return true;
+    }
+    if (t === "DoWhileStatement") { // before `while` keyword.
+        return true;
+    }
+    const nodeList = getChildren(node.parent);
+
+    return nodeList !== null && nodeList[nodeList.length - 1] === node; // before `}` or etc.
+}
+
 module.exports = {
     meta: {
         docs: {
@@ -40,23 +78,6 @@ module.exports = {
         const sourceCode = context.getSourceCode();
         const option = context.options[0] || "last";
 
-        /**
-         * Check whether comments exist between the given 2 tokens.
-         * @param {Token} left The left token to check.
-         * @param {Token} right The right token to check.
-         * @returns {boolean} `true` if comments exist between the given 2 tokens.
-         */
-        function commentsExistBetween(left, right) {
-            return sourceCode.getFirstTokenBetween(
-                left,
-                right,
-                {
-                    includeComments: true,
-                    filter: astUtils.isCommentToken
-                }
-            ) !== null;
-        }
-
         /**
          * Check the given semicolon token.
          * @param {Token} semiToken The semicolon token to check.
@@ -79,7 +100,7 @@ module.exports = {
                             : "the beginning of the next line"
                     },
                     fix(fixer) {
-                        if (prevToken && nextToken && commentsExistBetween(prevToken, nextToken)) {
+                        if (prevToken && nextToken && sourceCode.commentsExistBetween(prevToken, nextToken)) {
                             return null;
                         }
 
@@ -95,6 +116,10 @@ module.exports = {
 
         return {
             [SELECTOR](node) {
+                if (option === "first" && isLastChild(node)) {
+                    return;
+                }
+
                 const lastToken = sourceCode.getLastToken(node);
 
                 if (astUtils.isSemicolonToken(lastToken)) {
diff --git a/lib/util/safe-emitter.js b/lib/util/safe-emitter.js
index 242cbe429..2fa373cb9 100644
--- a/lib/util/safe-emitter.js
+++ b/lib/util/safe-emitter.js
@@ -10,7 +10,7 @@
 //------------------------------------------------------------------------------
 
 /**
- * An object describing an AST selector
+ * An event emitter
  * @typedef {Object} SafeEmitter
  * @property {function(eventName: string, listenerFunc: Function): void} on Adds a listener for a given event name
  * @property {function(eventName: string, arg1?: any, arg2?: any, arg3?: any)} emit Emits an event with a given name.
diff --git a/package.json b/package.json
index da71cf6d0..69f896149 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint",
-  "version": "4.10.0",
+  "version": "4.11.0",
   "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
   "description": "An AST-based pattern checker for JavaScript.",
   "bin": {
@@ -42,7 +42,7 @@
     "debug": "^3.0.1",
     "doctrine": "^2.0.0",
     "eslint-scope": "^3.7.1",
-    "espree": "^3.5.1",
+    "espree": "^3.5.2",
     "esquery": "^1.0.0",
     "estraverse": "^4.2.0",
     "esutils": "^2.0.2",
@@ -84,11 +84,11 @@
     "coveralls": "^2.13.1",
     "dateformat": "^2.0.0",
     "ejs": "^2.5.6",
-    "eslint-rule-composer": "^0.1.0",
     "eslint-plugin-eslint-plugin": "^1.2.0",
     "eslint-plugin-node": "^5.1.0",
     "eslint-plugin-rulesdir": "^0.1.0",
     "eslint-release": "^0.10.1",
+    "eslint-rule-composer": "^0.1.0",
     "eslump": "1.6.0",
     "esprima": "^4.0.0",
     "esprima-fb": "^15001.1001.0-dev-harmony-fb",
@@ -108,7 +108,6 @@
     "phantomjs-prebuilt": "^2.1.14",
     "proxyquire": "^1.8.0",
     "shelljs": "^0.7.7",
-    "shelljs-nodecli": "~0.1.1",
     "sinon": "^3.2.1",
     "temp": "^0.8.3",
     "through": "^2.3.8"
diff --git a/packages/eslint-config-eslint/LICENSE b/packages/eslint-config-eslint/LICENSE
index d41bdf795..777939e8f 100644
--- a/packages/eslint-config-eslint/LICENSE
+++ b/packages/eslint-config-eslint/LICENSE
@@ -1,5 +1,5 @@
 ESLint
-Copyright jQuery Foundation and other contributors, https://jquery.org/
+Copyright JS Foundation and other contributors, https://js.foundation
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index 9861998ff..aae66301d 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -271,6 +271,23 @@ class RuleTester {
             ].concat(scenarioErrors).join("\n"));
         }
 
+
+        linter.defineRule(ruleName, Object.assign({}, rule, {
+
+            // Create a wrapper rule that freezes the `context` properties.
+            create(context) {
+                freezeDeeply(context.options);
+                freezeDeeply(context.settings);
+                freezeDeeply(context.parserOptions);
+
+                return (typeof rule === "function" ? rule : rule.create)(context);
+            }
+        }));
+
+        linter.defineRules(this.rules);
+
+        const ruleMap = linter.getRules();
+
         /**
          * Run the rule for the given item
          * @param {string|Object} item Item to run the rule against
@@ -313,20 +330,22 @@ class RuleTester {
                 config.rules[ruleName] = 1;
             }
 
-            linter.defineRule(ruleName, Object.assign({}, rule, {
+            const schema = validator.getRuleOptionsSchema(rule);
 
-                // Create a wrapper rule that freezes the `context` properties.
-                create(context) {
-                    freezeDeeply(context.options);
-                    freezeDeeply(context.settings);
-                    freezeDeeply(context.parserOptions);
-
-                    return (typeof rule === "function" ? rule : rule.create)(context);
+            /*
+             * Setup AST getters.
+             * The goal is to check whether or not AST was modified when
+             * running the rule under test.
+             */
+            linter.defineRule("rule-tester/validate-ast", () => ({
+                Program(node) {
+                    beforeAST = cloneDeeplyExcludesParent(node);
+                },
+                "Program:exit"(node) {
+                    afterAST = node;
                 }
             }));
 
-            const schema = validator.getRuleOptionsSchema(ruleName, linter.rules);
-
             if (schema) {
                 ajv.validateSchema(schema);
 
@@ -341,21 +360,7 @@ class RuleTester {
                 }
             }
 
-            validator.validate(config, "rule-tester", linter.rules, new Environments());
-
-            /*
-             * Setup AST getters.
-             * The goal is to check whether or not AST was modified when
-             * running the rule under test.
-             */
-            linter.defineRule("rule-tester/validate-ast", () => ({
-                Program(node) {
-                    beforeAST = cloneDeeplyExcludesParent(node);
-                },
-                "Program:exit"(node) {
-                    afterAST = node;
-                }
-            }));
+            validator.validate(config, "rule-tester", ruleMap.get.bind(ruleMap), new Environments());
 
             return {
                 messages: linter.verify(code, config, filename, true),
@@ -532,7 +537,6 @@ class RuleTester {
             RuleTester.describe("valid", () => {
                 test.valid.forEach(valid => {
                     RuleTester.it(typeof valid === "object" ? valid.code : valid, () => {
-                        linter.defineRules(this.rules);
                         testValidTemplate(valid);
                     });
                 });
@@ -541,7 +545,6 @@ class RuleTester {
             RuleTester.describe("invalid", () => {
                 test.invalid.forEach(invalid => {
                     RuleTester.it(invalid.code, () => {
-                        linter.defineRules(this.rules);
                         testInvalidTemplate(invalid);
                     });
                 });
diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js
index 5f3d65278..5d6cf8cc0 100644
--- a/tests/lib/config/config-validator.js
+++ b/tests/lib/config/config-validator.js
@@ -90,6 +90,15 @@ const mockRequiredOptionsRule = {
 
 describe("Validator", () => {
 
+    /**
+     * Gets a loaded rule given a rule ID
+     * @param {string} ruleId The ID of the rule
+     * @returns {{create: Function}} The loaded rule
+     */
+    function ruleMapper(ruleId) {
+        return linter.getRules().get(ruleId);
+    }
+
     beforeEach(() => {
         linter.defineRule("mock-rule", mockRule);
         linter.defineRule("mock-required-options-rule", mockRequiredOptionsRule);
@@ -98,7 +107,7 @@ describe("Validator", () => {
     describe("validate", () => {
 
         it("should do nothing with an empty config", () => {
-            const fn = validator.validate.bind(null, {}, "tests", linter.rules, linter.environments);
+            const fn = validator.validate.bind(null, {}, "tests", ruleMapper, linter.environments);
 
             assert.doesNotThrow(fn);
         });
@@ -118,7 +127,7 @@ describe("Validator", () => {
                     rules: {}
                 },
                 "tests",
-                linter.rules,
+                ruleMapper,
                 linter.environments
             );
 
@@ -132,7 +141,7 @@ describe("Validator", () => {
                     foo: true
                 },
                 "tests",
-                linter.rules,
+                ruleMapper,
                 linter.environments
             );
 
@@ -141,13 +150,13 @@ describe("Validator", () => {
 
         describe("root", () => {
             it("should throw with a string value", () => {
-                const fn = validator.validate.bind(null, { root: "true" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { root: "true" }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `\"true\"`).");
             });
 
             it("should throw with a numeric value", () => {
-                const fn = validator.validate.bind(null, { root: 0 }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { root: 0 }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`).");
             });
@@ -155,13 +164,13 @@ describe("Validator", () => {
 
         describe("globals", () => {
             it("should throw with a string value", () => {
-                const fn = validator.validate.bind(null, { globals: "jQuery" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { globals: "jQuery" }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `\"jQuery\"`).");
             });
 
             it("should throw with an array value", () => {
-                const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`).");
             });
@@ -169,7 +178,7 @@ describe("Validator", () => {
 
         describe("parser", () => {
             it("should not throw with a null value", () => {
-                const fn = validator.validate.bind(null, { parser: null }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { parser: null }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -178,31 +187,31 @@ describe("Validator", () => {
         describe("env", () => {
 
             it("should throw with an array environment", () => {
-                const fn = validator.validate.bind(null, { env: [] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: [] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `[]`).");
             });
 
             it("should throw with a primitive environment", () => {
-                const fn = validator.validate.bind(null, { env: 1 }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: 1 }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `1`).");
             });
 
             it("should catch invalid environments", () => {
-                const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Environment key \"invalid\" is unknown\n");
             });
 
             it("should catch disabled invalid environments", () => {
-                const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Environment key \"invalid\" is unknown\n");
             });
 
             it("should do nothing with an undefined environment", () => {
-                const fn = validator.validate.bind(null, {}, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, {}, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -211,13 +220,13 @@ describe("Validator", () => {
 
         describe("plugins", () => {
             it("should not throw with an empty array", () => {
-                const fn = validator.validate.bind(null, { plugins: [] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { plugins: [] }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with a string", () => {
-                const fn = validator.validate.bind(null, { plugins: "react" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { plugins: "react" }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`).");
             });
@@ -225,13 +234,13 @@ describe("Validator", () => {
 
         describe("settings", () => {
             it("should not throw with an empty object", () => {
-                const fn = validator.validate.bind(null, { settings: {} }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { settings: {} }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with an array", () => {
-                const fn = validator.validate.bind(null, { settings: ["foo"] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { settings: ["foo"] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`).");
             });
@@ -239,19 +248,19 @@ describe("Validator", () => {
 
         describe("extends", () => {
             it("should not throw with an empty array", () => {
-                const fn = validator.validate.bind(null, { extends: [] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { extends: [] }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should not throw with a string", () => {
-                const fn = validator.validate.bind(null, { extends: "react" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { extends: "react" }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with an object", () => {
-                const fn = validator.validate.bind(null, { extends: {} }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { extends: {} }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"extends\" is the wrong type (expected string/array but got `{}`).");
             });
@@ -259,13 +268,13 @@ describe("Validator", () => {
 
         describe("parserOptions", () => {
             it("should not throw with an empty object", () => {
-                const fn = validator.validate.bind(null, { parserOptions: {} }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { parserOptions: {} }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with an array", () => {
-                const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`).");
             });
@@ -274,67 +283,67 @@ describe("Validator", () => {
         describe("rules", () => {
 
             it("should do nothing with an empty rules object", () => {
-                const fn = validator.validate.bind(null, { rules: {} }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: {} }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config with rules", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": [2, "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": [2, "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is off", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["off", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["off", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with an invalid config when severity is off", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": "off" } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": "off" } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with an invalid config when severity is an array with 'off'", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": ["off"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": ["off"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is warn", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["warn", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["warn", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is error", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["error", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["error", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is Off", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Off", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Off", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is Warn", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Warn", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Warn", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is Error", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Error", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Error", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should catch invalid rule options", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n");
             });
@@ -342,7 +351,7 @@ describe("Validator", () => {
             it("should allow for rules with no options", () => {
                 linter.defineRule("mock-no-options-rule", mockNoOptionsRule);
 
-                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": 2 } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": 2 } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -350,7 +359,7 @@ describe("Validator", () => {
             it("should not allow options for rules with no options", () => {
                 linter.defineRule("mock-no-options-rule", mockNoOptionsRule);
 
-                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n");
             });
@@ -358,43 +367,43 @@ describe("Validator", () => {
 
         describe("overrides", () => {
             it("should not throw with an empty overrides array", () => {
-                const fn = validator.validate.bind(null, { overrides: [] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [] }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should not throw with a valid overrides array", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: {} }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: {} }] }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw if override does not specify files", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- \"overrides[0]\" should have required property 'files'. Value: {\"rules\":{}}.\n");
             });
 
             it("should throw if override has an empty files array", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have less than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n");
             });
 
             it("should throw if override has nested overrides", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].overrides\".\n");
             });
 
             it("should throw if override extends", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].extends\".\n");
             });
 
             it("should throw if override tries to set root", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n");
             });
@@ -405,12 +414,12 @@ describe("Validator", () => {
     describe("getRuleOptionsSchema", () => {
 
         it("should return null for a missing rule", () => {
-            assert.strictEqual(validator.getRuleOptionsSchema("non-existent-rule", linter.rules), null);
+            assert.strictEqual(validator.getRuleOptionsSchema(linter.rules.get("non-existent-rule")), null);
         });
 
         it("should not modify object schema", () => {
             linter.defineRule("mock-object-rule", mockObjectRule);
-            assert.deepStrictEqual(validator.getRuleOptionsSchema("mock-object-rule", linter.rules), {
+            assert.deepStrictEqual(validator.getRuleOptionsSchema(linter.rules.get("mock-object-rule")), {
                 enum: ["first", "second"]
             });
         });
@@ -420,43 +429,43 @@ describe("Validator", () => {
     describe("validateRuleOptions", () => {
 
         it("should throw for incorrect warning level number", () => {
-            const fn = validator.validateRuleOptions.bind(null, "mock-rule", 3, "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("mock-rule"), "mock-rule", 3, "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n");
         });
 
         it("should throw for incorrect warning level string", () => {
-            const fn = validator.validateRuleOptions.bind(null, "mock-rule", "booya", "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("mock-rule"), "mock-rule", "booya", "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '\"booya\"').\n");
         });
 
         it("should throw for invalid-type warning level", () => {
-            const fn = validator.validateRuleOptions.bind(null, "mock-rule", [["error"]], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("mock-rule"), "mock-rule", [["error"]], "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '[ \"error\" ]').\n");
         });
 
         it("should only check warning level for nonexistent rules", () => {
-            const fn = validator.validateRuleOptions.bind(null, "non-existent-rule", [3, "foobar"], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("non-existent-rule"), "non-existent-rule", [3, "foobar"], "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n");
         });
 
         it("should only check warning level for plugin rules", () => {
-            const fn = validator.validateRuleOptions.bind(null, "plugin/rule", 3, "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("plugin/rule"), "plugin/rule", 3, "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"plugin/rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n");
         });
 
         it("should throw for incorrect configuration values", () => {
-            const fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "frist"], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("mock-rule"), "mock-rule", [2, "frist"], "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" should be equal to one of the allowed values.\n");
         });
 
         it("should throw for too many configuration values", () => {
-            const fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "first", "second"], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("mock-rule"), "mock-rule", [2, "first", "second"], "tests");
 
             assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue [\"first\",\"second\"] should NOT have more than 1 items.\n");
         });
diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js
index 82aa2a40a..5d279d5a3 100644
--- a/tests/lib/rules/no-restricted-imports.js
+++ b/tests/lib/rules/no-restricted-imports.js
@@ -35,6 +35,134 @@ ruleTester.run("no-restricted-imports", rule, {
         {
             code: "import withGitignores from \"foo/bar\";",
             options: [{ patterns: ["foo/*", "!foo/bar"] }]
+        },
+        {
+            code: "import AllowedObject from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"]
+                }]
+            }]
+        },
+        {
+            code: "import DisallowedObject from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"]
+                }]
+            }]
+        },
+        {
+            code: "import * as DisallowedObject from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "bar",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import { AllowedObject } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import { DisallowedObject } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "bar",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import { AllowedObject as DisallowedObject } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import { AllowedObject, AllowedObjectTwo } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import { AllowedObject, AllowedObjectTwo  as DisallowedObject } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import AllowedObjectThree, { AllowedObject as AllowedObjectTwo } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import AllowedObject, { AllowedObjectTwo as DisallowedObject } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import AllowedObject, { AllowedObjectTwo as DisallowedObject } from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject", "DisallowedObjectTwo"],
+                    message: "Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import AllowedObject, * as DisallowedObject from \"foo\";",
+            options: [{
+                paths: [{
+                    name: "bar",
+                    importNames: ["DisallowedObject"],
+                    message: "Please import 'DisallowedObject' from /bar/ instead."
+                }]
+            }]
+        },
+        {
+            code: "import \"foo\";",
+            options: [{
+                paths: [{
+                    name: "foo",
+                    importNames: ["DisallowedObject", "DisallowedObjectTwo"],
+                    message: "Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead."
+                }]
+            }]
         }
     ],
     invalid: [{
@@ -55,7 +183,11 @@ ruleTester.run("no-restricted-imports", rule, {
         errors: [{ message: "'foo/bar' import is restricted from being used.", type: "ImportDeclaration" }]
     }, {
         code: "import withPatterns from \"foo/bar\";",
-        options: [{ patterns: ["foo/*"] }],
+        options: [{ patterns: ["foo"] }],
+        errors: [{ message: "'foo/bar' import is restricted from being used by a pattern.", type: "ImportDeclaration" }]
+    }, {
+        code: "import withPatterns from \"foo/bar\";",
+        options: [{ patterns: ["bar"] }],
         errors: [{ message: "'foo/bar' import is restricted from being used by a pattern.", type: "ImportDeclaration" }]
     }, {
         code: "import withGitignores from \"foo/bar\";",
@@ -97,5 +229,174 @@ ruleTester.run("no-restricted-imports", rule, {
             message: "'foo' import is restricted from being used. Please import from 'bar' instead.",
             type: "ImportDeclaration"
         }]
-    }]
+    },
+    {
+        code: "import DisallowedObject from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["default"],
+                message: "Please import the default import of 'foo' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import the default import of 'foo' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import * as All from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "* import is invalid because 'DisallowedObject' from 'foo' is restricted.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import { DisallowedObject } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import { DisallowedObject as AllowedObject } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import { AllowedObject, DisallowedObject } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import { AllowedObject, DisallowedObject as AllowedObjectTwo } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import { AllowedObject, DisallowedObject as AllowedObjectTwo } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObjectTwo", "DisallowedObject"],
+                message: "Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import { AllowedObject, DisallowedObject as AllowedObjectTwo } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject", "DisallowedObjectTwo"],
+                message: "Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import DisallowedObject, { AllowedObject as AllowedObjectTwo } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["default"],
+                message: "Please import the default import of 'foo' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import the default import of 'foo' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import AllowedObject, { DisallowedObject as AllowedObjectTwo } from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "'foo' import is restricted from being used. Please import 'DisallowedObject' from /bar/ instead.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import AllowedObject, * as AllowedObjectTwo from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject"],
+                message: "Please import 'DisallowedObject' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "* import is invalid because 'DisallowedObject' from 'foo' is restricted.",
+            type: "ImportDeclaration"
+        }]
+    },
+    {
+        code: "import AllowedObject, * as AllowedObjectTwo from \"foo\";",
+        options: [{
+            paths: [{
+                name: "foo",
+                importNames: ["DisallowedObject", "DisallowedObjectTwo"],
+                message: "Please import 'DisallowedObject' and 'DisallowedObjectTwo' from /bar/ instead."
+            }]
+        }],
+        errors: [{
+            message: "* import is invalid because 'DisallowedObject,DisallowedObjectTwo' from 'foo' is restricted.",
+            type: "ImportDeclaration"
+        }]
+    }
+    ]
 });
diff --git a/tests/lib/rules/semi-style.js b/tests/lib/rules/semi-style.js
index 43968facf..8b0834af3 100644
--- a/tests/lib/rules/semi-style.js
+++ b/tests/lib/rules/semi-style.js
@@ -27,18 +27,103 @@ ruleTester.run("semi-style", rule, {
         "for(a;\nb;\nc);",
         "for((a\n);\n(b\n);\n(c));",
         "if(a)foo;\nbar",
+        { code: ";", options: ["last"] },
         { code: ";foo;bar;baz;", options: ["last"] },
         { code: "foo;\nbar;", options: ["last"] },
         { code: "for(a;b;c);", options: ["last"] },
         { code: "for(a;\nb;\nc);", options: ["last"] },
         { code: "for((a\n);\n(b\n);\n(c));", options: ["last"] },
         { code: "if(a)foo;\nbar", options: ["last"] },
+        { code: ";", options: ["first"] },
         { code: ";foo;bar;baz;", options: ["first"] },
         { code: "foo\n;bar;", options: ["first"] },
         { code: "for(a;b;c);", options: ["first"] },
         { code: "for(a;\nb;\nc);", options: ["first"] },
         { code: "for((a\n);\n(b\n);\n(c));", options: ["first"] },
-        { code: "if(a)foo\n;bar", options: ["first"] }
+
+        // edge cases
+        {
+            code: `
+                {
+                    ;
+                }
+            `,
+            options: ["first"]
+        },
+        {
+            code: `
+                while (a)
+                    ;
+                foo
+            `,
+            options: ["first"]
+        },
+        {
+            code: `
+                do
+                    ;
+                while (a)
+            `,
+            options: ["first"]
+        },
+        {
+            code: `
+                do
+                    foo;
+                while (a)
+            `,
+            options: ["first"]
+        },
+        {
+            code: `
+                if (a)
+                    foo;
+                else
+                    bar
+            `,
+            options: ["first"]
+        },
+        {
+            code: `
+                if (a)
+                    foo
+                ;bar
+            `,
+            options: ["first"]
+        },
+        {
+            code: `
+                {
+                    ;
+                }
+            `,
+            options: ["last"]
+        },
+        {
+            code: `
+                switch (a) {
+                    case 1:
+                        ;foo
+                }
+            `,
+            options: ["last"]
+        },
+        {
+            code: `
+                while (a)
+                    ;
+                foo
+            `,
+            options: ["last"]
+        },
+        {
+            code: `
+                do
+                    ;
+                while (a)
+            `,
+            options: ["last"]
+        }
     ],
     invalid: [
         {