From 6d4351228e5d762c75e7db73a7311d21a1f7c681 Mon Sep 17 00:00:00 2001 From: Houssein Djirdeh Date: Mon, 10 May 2021 14:35:11 -0400 Subject: [PATCH] ESLint Plugin: passHref is not assigned (#24670) Adds a lint rule warning to the Next.js ESLint plugin if `passHref=true` is not assigned for `` wrapping a custom component. Fixes #23713 --- errors/link-passhref.md | 32 ++++++++ errors/manifest.json | 4 + packages/eslint-plugin-next/lib/index.js | 2 + .../lib/rules/link-passhref.js | 65 +++++++++++++++ .../link-passhref.unit.test.js | 79 +++++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 errors/link-passhref.md create mode 100644 packages/eslint-plugin-next/lib/rules/link-passhref.js create mode 100644 test/eslint-plugin-next/link-passhref.unit.test.js diff --git a/errors/link-passhref.md b/errors/link-passhref.md new file mode 100644 index 0000000000000..33fec658066e0 --- /dev/null +++ b/errors/link-passhref.md @@ -0,0 +1,32 @@ +# Link passHref + +### Why This Error Occurred + +`passHref` was not used for a `Link` component that wraps a custom component. This is needed in order to pass the `href` to the child `` tag. + +### Possible Ways to Fix It + +If you're using a custom component that wraps an `` tag, make sure to add `passHref`: + +```jsx +import Link from 'next/link' +import styled from 'styled-components' + +const StyledLink = styled.a` + color: red; +` + +function NavLink({ href, name }) { + return ( + + {name} + + ) +} + +export default NavLink +``` + +### Useful Links + +- [next/link - Custom Component](https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag) diff --git a/errors/manifest.json b/errors/manifest.json index 593779eb6fc25..b0ccb3e18df37 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -207,6 +207,10 @@ "title": "invalid-webpack-5-version", "path": "/errors/invalid-webpack-5-version.md" }, + { + "title": "link-passhref", + "path": "/errors/link-passhref.md" + }, { "title": "manifest.json", "path": "/errors/manifest.json" }, { "title": "minification-disabled", diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index 3212e7a21e8a3..bcc6f65bf7fee 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -7,6 +7,7 @@ module.exports = { 'no-title-in-document-head': require('./rules/no-title-in-document-head'), 'google-font-display': require('./rules/google-font-display'), 'google-font-preconnect': require('./rules/google-font-preconnect'), + 'link-passhref': require('./rules/link-passhref'), }, configs: { recommended: { @@ -19,6 +20,7 @@ module.exports = { '@next/next/no-title-in-document-head': 1, '@next/next/google-font-display': 1, '@next/next/google-font-preconnect': 1, + '@next/next/link-passhref': 1, }, }, }, diff --git a/packages/eslint-plugin-next/lib/rules/link-passhref.js b/packages/eslint-plugin-next/lib/rules/link-passhref.js new file mode 100644 index 0000000000000..cfde6bdc943e3 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/link-passhref.js @@ -0,0 +1,65 @@ +const NodeAttributes = require('../utils/nodeAttributes.js') + +module.exports = { + meta: { + docs: { + description: + 'Ensure passHref is assigned if child of Link component is a custom component', + category: 'HTML', + recommended: true, + }, + fixable: null, + }, + + create: function (context) { + let linkImport = null + + return { + ImportDeclaration(node) { + if (node.source.value === 'next/link') { + linkImport = node.specifiers[0].local.name + } + }, + + JSXOpeningElement(node) { + if (node.name.name !== 'Link' || node.name.name !== linkImport) { + return + } + + const attributes = new NodeAttributes(node) + const children = node.parent.children + + if ( + !attributes.hasAny() || + !attributes.has('href') || + !children.some((attr) => attr.type === 'JSXElement') + ) { + return + } + + const hasPassHref = + attributes.has('passHref') && + (typeof attributes.value('passHref') === 'undefined' || + attributes.value('passHref') === true) + + const hasAnchorChild = children.some( + (attr) => + attr.type === 'JSXElement' && + attr.openingElement.name.name === 'a' && + attr.closingElement.name.name === 'a' + ) + + if (!hasAnchorChild && !hasPassHref) { + context.report({ + node, + message: `passHref ${ + attributes.value('passHref') !== true + ? 'must be set to true' + : 'is missing' + }. See https://nextjs.org/docs/messages/link-passhref.`, + }) + } + }, + } + }, +} diff --git a/test/eslint-plugin-next/link-passhref.unit.test.js b/test/eslint-plugin-next/link-passhref.unit.test.js new file mode 100644 index 0000000000000..b980fff496917 --- /dev/null +++ b/test/eslint-plugin-next/link-passhref.unit.test.js @@ -0,0 +1,79 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/link-passhref') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('link-passhref', rule, { + valid: [ + `export const Home = () => ( + + )`, + + `export const Home = () => ( + + Test + + )`, + + `export const Home = () => ( + + Test + + )`, + + `const Link = () =>
Fake Link
+ + const Home = () => ( + + + + )`, + ], + + invalid: [ + { + code: ` + import Link from 'next/link' + + export const Home = () => ( + + Test + + )`, + errors: [ + { + message: + 'passHref is missing. See https://nextjs.org/docs/messages/link-passhref.', + type: 'JSXOpeningElement', + }, + ], + }, + { + code: ` + import Link from 'next/link' + + export const Home = () => ( + + Test + + )`, + errors: [ + { + message: + 'passHref must be set to true. See https://nextjs.org/docs/messages/link-passhref.', + type: 'JSXOpeningElement', + }, + ], + }, + ], +})