diff --git a/.changeset/curvy-stingrays-decide.md b/.changeset/curvy-stingrays-decide.md new file mode 100644 index 00000000..16503a83 --- /dev/null +++ b/.changeset/curvy-stingrays-decide.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-primer-react": patch +--- + +Add rule for Link to not be allowed without href diff --git a/src/rules/__tests__/enforce-button-for-link-with-nohref.test.js b/src/rules/__tests__/enforce-button-for-link-with-nohref.test.js new file mode 100644 index 00000000..847d192c --- /dev/null +++ b/src/rules/__tests__/enforce-button-for-link-with-nohref.test.js @@ -0,0 +1,86 @@ +const rule = require('../enforce-button-for-link-with-nohref') +const {RuleTester} = require('eslint') + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, +}) + +ruleTester.run('enforce-button-for-link-with-nohref', rule, { + valid: [ + // Link with href attribute + `import {Link} from '@primer/react'; + Valid Link`, + + // Link with href and inline prop + `import {Link} from '@primer/react'; + Valid Inline Link`, + + // Link with href and className + `import {Link} from '@primer/react'; + Valid Link with Class`, + + // Link with href, inline, and className + `import {Link} from '@primer/react'; + Valid Inline Link with Class`, + + // Link with href as variable + `import {Link} from '@primer/react'; + const url = '/about'; + About`, + + // Button component (not Link) + `import {Button} from '@primer/react'; + `, + + // Regular HTML link (not Primer Link) + `Click me`, + + // Link from different package + `import {Link} from 'react-router-dom'; + About`, + ], + invalid: [ + { + code: `import {Link} from '@primer/react'; + Invalid Link without href`, + errors: [ + { + messageId: 'noLinkWithoutHref', + }, + ], + }, + { + code: `import {Link} from '@primer/react'; + Invalid Link with class but no href`, + errors: [ + { + messageId: 'noLinkWithoutHref', + }, + ], + }, + { + code: `import {Link} from '@primer/react'; + Invalid inline Link without href`, + errors: [ + { + messageId: 'noLinkWithoutHref', + }, + ], + }, + { + code: `import {Link} from '@primer/react'; + Invalid Link with onClick but no href`, + errors: [ + { + messageId: 'noLinkWithoutHref', + }, + ], + }, + ], +}) diff --git a/src/rules/enforce-button-for-link-with-nohref.js b/src/rules/enforce-button-for-link-with-nohref.js new file mode 100644 index 00000000..957169de --- /dev/null +++ b/src/rules/enforce-button-for-link-with-nohref.js @@ -0,0 +1,41 @@ +const url = require('../url') +const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute') +const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name') +const {isPrimerComponent} = require('../utils/is-primer-component') + +module.exports = { + meta: { + type: 'error', + docs: { + description: 'Disallow usage of Link component without href', + recommended: true, + url: url(module), + }, + messages: { + noLinkWithoutHref: 'Links without href and other side effects are not accessible. Use a Button instead.', + }, + }, + + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode() + return { + JSXElement(node) { + const openingElement = node.openingElement + const elementName = getJSXOpeningElementName(openingElement) + + // Check if this is a Link component from @primer/react + if (elementName === 'Link' && isPrimerComponent(openingElement.name, sourceCode.getScope(node))) { + // Check if the Link has an href attribute + const hrefAttribute = getJSXOpeningElementAttribute(openingElement, 'href') + + if (!hrefAttribute) { + context.report({ + node: openingElement, + messageId: 'noLinkWithoutHref', + }) + } + } + }, + } + }, +}