Repository: yannickcr/eslint-plugin-react
Pull request #1784 (2018-05-11 21:24:03)

Target blank dynamic link should fail
Made from the suggestion in @ksdmitrieva's [issue](https://github.com/yannickcr/eslint-plugin-react/issues/1737), I believe this is the right approach even though you would get false positives since it cannot check for relative paths in dynamic links. However, I think that would be rare since it's not best practice to use `target='_blank'` on internal links anyway.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32d9e1ab..69337727 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,20 +3,6 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
 
-## [7.8.2] - 2018-05-13
-### Fixed
-* Fix crash in [`boolean-prop-naming`][] when encountering a required shape prop type ([#1791][] @pcorpet)
-
-[7.8.2]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.8.1...v7.8.2
-[#1791]: https://github.com/yannickcr/eslint-plugin-react/issues/1791
-
-## [7.8.1] - 2018-05-12
-### Fixed
-* Fix crash in [`no-deprecated`][] when encountering a class constructor ([#1785][] @taddei)
-
-[7.8.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.8.0...v7.8.1
-[#1785]: https://github.com/yannickcr/eslint-plugin-react/issues/1785
-
 ## [7.8.0] - 2018-05-11
 ### Added
 * Add support for fragments to [`react-in-jsx-scope`][] ([#1758][])
diff --git a/docs/rules/jsx-no-target-blank.md b/docs/rules/jsx-no-target-blank.md
index ae92669f..906e852b 100644
--- a/docs/rules/jsx-no-target-blank.md
+++ b/docs/rules/jsx-no-target-blank.md
@@ -4,14 +4,28 @@ When creating a JSX element that has an `a` tag, it is often desired to have
 the link open in a new tab using the `target='_blank'` attribute. Using this
 attribute unaccompanied by `rel='noreferrer noopener'`, however, is a severe
 security vulnerability ([see here for more details](https://mathiasbynens.github.io/rel-noopener))
-This rules requires that you accompany all `target='_blank'` attributes with `rel='noreferrer noopener'`.
+This rules requires that you accompany `target='_blank'` attributes with `rel='noreferrer noopener'`.
 
 ## Rule Details
 
-The following patterns are considered errors:
+This rule aims to prevent user generated links from creating security vulerabilities by requiring
+`rel='noreferrer noopener'` for external links, and optionally any dynamically generated links.
+
+## Rule Options
+
+There are two main options for the rule:
+
+* `{"enforceDynamicLinks": "always"}` enforces the rule if the href is a dyanamic link (default)
+* `{"enforceDynamicLinks": "never"}` does not enforce the rule if the href is a dyamic link
+
+
+### always (default)
+
+When {"enforceDynamicLinks": "always"} is set, the following patterns are considered errors:
 
 ```jsx
 var Hello = <a target='_blank' href="http://example.com/"></a>
+var Hello = <a target='_blank' href={ dynamicLink }></a>
 ```
 
 The following patterns are **not** considered errors:
@@ -24,6 +38,14 @@ var Hello = <a target='_blank' href="/absolute/path/in/the/host"></a>
 var Hello = <a></a>
 ```
 
+### never
+
+When {"enforceDynamicLinks": "never"} is set, the following patterns are **not** considered errors:
+
+```jsx
+var Hello = <a target='_blank' href={ dynamicLink }></a>
+```
+
 ## When Not To Use It
 
 If you do not have any external links, you can disable this rule
diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js
index 3d414a72..ccfdf6f3 100644
--- a/lib/rules/boolean-prop-naming.js
+++ b/lib/rules/boolean-prop-naming.js
@@ -76,10 +76,7 @@ module.exports = {
       if (node.value.property) {
         const name = node.value.property.name;
         if (name === 'isRequired') {
-          if (node.value.object && node.value.object.property) {
-            return node.value.object.property.name;
-          }
-          return null;
+          return node.value.object.property.name;
         }
         return name;
       }
diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js
index a5b34c2d..e4492874 100644
--- a/lib/rules/jsx-no-target-blank.js
+++ b/lib/rules/jsx-no-target-blank.js
@@ -23,6 +23,12 @@ function hasExternalLink(element) {
       /^(?:\w+:|\/\/)/.test(attr.value.value));
 }
 
+function hasDynamicLink(element) {
+  return element.attributes.some(attr => attr.name &&
+    attr.name.name === 'href' &&
+    attr.value.type === 'JSXExpressionContainer');
+}
+
 function hasSecureRel(element) {
   return element.attributes.find(attr => {
     if (attr.type === 'JSXAttribute' && attr.name.name === 'rel') {
@@ -41,21 +47,28 @@ module.exports = {
       recommended: true,
       url: docsUrl('jsx-no-target-blank')
     },
-    schema: []
+    schema: [{
+      type: 'object',
+      properties: {
+        enforceDynamicLinks: {
+          enum: ['always', 'never']
+        }
+      },
+      additionalProperties: false
+    }]
   },
 
   create: function(context) {
+    const configuration = context.options[0] || {};
+    const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
+
     return {
       JSXAttribute: function(node) {
-        if (node.parent.name.name !== 'a') {
+        if (node.parent.name.name !== 'a' || !isTargetBlank(node) || hasSecureRel(node.parent)) {
           return;
         }
 
-        if (
-          isTargetBlank(node) &&
-          hasExternalLink(node.parent) &&
-          !hasSecureRel(node.parent)
-        ) {
+        if (hasExternalLink(node.parent) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))) {
           context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' +
           'is a security risk: see https://mathiasbynens.github.io/rel-noopener');
         }
diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js
index c5dee54f..6072290f 100644
--- a/lib/rules/no-deprecated.js
+++ b/lib/rules/no-deprecated.js
@@ -101,7 +101,6 @@ module.exports = {
       return (
         deprecated &&
         deprecated[method] &&
-        deprecated[method][0] &&
         versionUtil.testReactVersion(context, deprecated[method][0])
       );
     }
diff --git a/lib/util/version.js b/lib/util/version.js
index 252d4f59..cbdec7d9 100644
--- a/lib/util/version.js
+++ b/lib/util/version.js
@@ -27,7 +27,7 @@ function getFlowVersionFromContext(context) {
 }
 
 function test(context, methodVer, confVer) {
-  methodVer = String(methodVer || '').split('.').map(part => Number(part));
+  methodVer = methodVer.split('.').map(part => Number(part));
   const higherMajor = methodVer[0] < confVer[0];
   const higherMinor = methodVer[0] === confVer[0] && methodVer[1] < confVer[1];
   const higherOrEqualPatch = methodVer[0] === confVer[0] && methodVer[1] === confVer[1] && methodVer[2] <= confVer[2];
diff --git a/package.json b/package.json
index 917be882..6914cb64 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-react",
-  "version": "7.8.2",
+  "version": "7.8.0",
   "author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
   "description": "React specific linting rules for ESLint",
   "main": "index.js",


Test patch:
diff --git a/tests/lib/rules/boolean-prop-naming.js b/tests/lib/rules/boolean-prop-naming.js
index e90fc397..f83c8dd9 100644
--- a/tests/lib/rules/boolean-prop-naming.js
+++ b/tests/lib/rules/boolean-prop-naming.js
@@ -348,17 +348,6 @@ ruleTester.run('boolean-prop-naming', rule, {
         hasValue: PropTypes.bool.isRequired
       }
     `
-  }, {
-    // Ensure the rule does not throw when a shape prop isRequired.
-    code: `
-      var Hello = createReactClass({
-        propTypes: {something: PropTypes.shape({}).isRequired},
-        render: function() { return <div />; }
-      });
-    `,
-    options: [{
-      rule: '^is[A-Z]([A-Za-z0-9]?)+'
-    }]
   }],
 
   invalid: [{
diff --git a/tests/lib/rules/jsx-no-target-blank.js b/tests/lib/rules/jsx-no-target-blank.js
index a0c756cb..aba9d080 100644
--- a/tests/lib/rules/jsx-no-target-blank.js
+++ b/tests/lib/rules/jsx-no-target-blank.js
@@ -25,6 +25,11 @@ const parserOptions = {
 // ------------------------------------------------------------------------------
 
 const ruleTester = new RuleTester({parserOptions});
+const defaultErrors = [{
+  message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
+  ' see https://mathiasbynens.github.io/rel-noopener'
+}];
+
 ruleTester.run('jsx-no-target-blank', rule, {
   valid: [
     {code: '<a href="foobar"></a>'},
@@ -38,61 +43,45 @@ ruleTester.run('jsx-no-target-blank', rule, {
     {code: '<a target="_blank" rel={relValue}></a>'},
     {code: '<a target={targetValue} rel="noopener noreferrer"></a>'},
     {code: '<a target={targetValue} href="relative/path"></a>'},
-    {code: '<a target={targetValue} href="/absolute/path"></a>'}
+    {code: '<a target={targetValue} href="/absolute/path"></a>'},
+    {
+      code: '<a target="_blank" href={ dynamicLink }></a>',
+      options: [{enforceDynamicLinks: 'never'}]
+    }
   ],
   invalid: [{
     code: '<a target="_blank" href="http://example.com"></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" rel="" href="http://example.com"></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" rel="noopenernoreferrer" href="http://example.com"></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_BLANK" href="http://example.com"></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" href="//example.com"></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" href="//example.com" rel={true}></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" href="//example.com" rel={3}></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" href="//example.com" rel={null}></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
   }, {
     code: '<a target="_blank" href="//example.com" rel></a>',
-    errors: [{
-      message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk:' +
-      ' see https://mathiasbynens.github.io/rel-noopener'
-    }]
+    errors: defaultErrors
+  }, {
+    code: '<a target="_blank" href={ dynamicLink }></a>',
+    errors: defaultErrors
+  }, {
+    code: '<a target="_blank" href={ dynamicLink }></a>',
+    options: [{enforceDynamicLinks: 'always'}],
+    errors: defaultErrors
   }]
 });
diff --git a/tests/lib/rules/no-deprecated.js b/tests/lib/rules/no-deprecated.js
index a1165563..b1b5630c 100644
--- a/tests/lib/rules/no-deprecated.js
+++ b/tests/lib/rules/no-deprecated.js
@@ -75,7 +75,6 @@ ruleTester.run('no-deprecated', rule, {
     {
       code: `
         class Foo {
-          constructor() {}
           componentWillMount() {}
           componentWillReceiveProps() {}
           componentWillUpdate() {}
@@ -380,35 +379,6 @@ ruleTester.run('no-deprecated', rule, {
           )
         }
       ]
-    },
-    {
-      code: `
-        class Foo extends React.Component {
-          constructor() {}
-          componentWillMount() {}
-          componentWillReceiveProps() {}
-          componentWillUpdate() {}
-        }
-      `,
-      errors: [
-        {
-          message: errorMessage(
-            'componentWillMount', '16.3.0', 'UNSAFE_componentWillMount',
-            'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount'
-          )
-        },
-        {
-          message: errorMessage(
-            'componentWillReceiveProps', '16.3.0', 'UNSAFE_componentWillReceiveProps',
-            'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops'
-          )
-        },
-        {
-          message: errorMessage('componentWillUpdate', '16.3.0', 'UNSAFE_componentWillUpdate',
-            'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate'
-          )
-        }
-      ]
     }
   ]
 });