diff --git a/package-lock.json b/package-lock.json index c7419cbb..a27f065f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eslint-plugin-primer-react", - "version": "1.0.1", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eslint-plugin-primer-react", - "version": "1.0.1", + "version": "3.0.0", "license": "MIT", "dependencies": { "@styled-system/props": "^5.1.5", diff --git a/src/rules/__tests__/implicit-heading.test.js b/src/rules/__tests__/implicit-heading.test.js new file mode 100644 index 00000000..b773c19e --- /dev/null +++ b/src/rules/__tests__/implicit-heading.test.js @@ -0,0 +1,52 @@ +const rule = require('../explicit-heading') +const {RuleTester} = require('eslint') + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + } +}) + +ruleTester.run('explicit-heading', rule, { + valid: [ + `import {Heading} from '@primer/react'; + Heading level 1 + `, + `import {Heading} from '@primer/react'; + Heading level 2 + `, + `import {Heading} from '@primer/react'; + Heading level 2 + `, +], +invalid: [ + { + code: ` + import {Heading} from '@primer/react'; + + Heading without "as" + `, + errors: [ + { + messageId: 'nonExplicitHeadingLevel' + } + ] + }, + { + code: ` + import {Heading} from '@primer/react'; + + Heading component used as "span" + `, + errors: [ + { + messageId: 'invalidAsValue' + } + ] + }, +] +}) diff --git a/src/rules/explicit-heading.js b/src/rules/explicit-heading.js new file mode 100644 index 00000000..ffb8f1a1 --- /dev/null +++ b/src/rules/explicit-heading.js @@ -0,0 +1,60 @@ +const {isPrimerComponent} = require('../utils/is-primer-component') +const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name') +const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute') + +const validHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + +const isHeading = elem => getJSXOpeningElementName(elem) === 'Heading' +const isUsingAs = elem => { + const asUsage = getJSXOpeningElementAttribute(elem, 'as'); + + if (!asUsage) return; + + return asUsage.value; +} + +const isValidAs = value => validHeadings.includes(value.toLowerCase()); +const isInvalid = elem => { + const elemAs = isUsingAs(elem); + + if (!elemAs) return 'nonExplicitHeadingLevel'; + if(!isValidAs(elemAs.value)) return 'invalidAsValue'; + + return false; +} + +module.exports = { + meta: { + type: "problem", + schema: [ + { + properties: { + skipImportCheck: { + type: 'boolean' + } + } + } + ], + messages: { + nonExplicitHeadingLevel: "Heading must have an explicit heading level applied through `as` prop.", + invalidAsValue: "Usage of `as` must only be used for headings (h1-h6)." + } + }, + create: function(context) { + return { + // callback functions + JSXOpeningElement(jsxNode) { + if (isPrimerComponent(jsxNode.name, context.getScope(jsxNode)) && isHeading(jsxNode)) { + const error = isInvalid(jsxNode); + + if (error) { + context.report({ + node: jsxNode, + messageId: error + }) + } + } + } + }; + } +}; \ No newline at end of file