eslint-plugin-primer-react 5.1.0 → 5.2.0-rc.dc08e82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "5.1.0",
3
+ "version": "5.2.0-rc.dc08e82",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@styled-system/props": "^5.1.5",
32
- "eslint-plugin-github": "^4.10.1",
32
+ "eslint-plugin-github": "^5.0.1",
33
33
  "eslint-plugin-jsx-a11y": "^6.7.1",
34
34
  "eslint-traverse": "^1.0.0",
35
35
  "lodash": "^4.17.21",
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ module.exports = {
7
7
  'new-color-css-vars': require('./rules/new-color-css-vars'),
8
8
  'a11y-explicit-heading': require('./rules/a11y-explicit-heading'),
9
9
  'no-deprecated-props': require('./rules/no-deprecated-props'),
10
+ 'a11y-link-in-text-block': require('./rules/a11y-link-in-text-block'),
10
11
  },
11
12
  configs: {
12
13
  recommended: require('./configs/recommended'),
@@ -0,0 +1,161 @@
1
+ const rule = require('../a11y-link-in-text-block')
2
+ const {RuleTester} = require('eslint')
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 'latest',
7
+ sourceType: 'module',
8
+ ecmaFeatures: {
9
+ jsx: true,
10
+ },
11
+ },
12
+ })
13
+
14
+ ruleTester.run('a11y-link-in-text-block', rule, {
15
+ valid: [
16
+ `import {Link} from '@primer/react';
17
+ <Box>
18
+
19
+ <Link href="something">
20
+ Blah blah
21
+ </Link>{' '}
22
+ .
23
+ </Box>
24
+ `,
25
+ `import {Text, Link} from '@primer/react';
26
+ <Something>
27
+ <Link href='blah'>
28
+ blah
29
+ </Link>
30
+ </Something>
31
+ `,
32
+ `import {Link} from '@primer/react';
33
+ <p>bla blah <Link inline={true}>Link level 1</Link></p>;
34
+ `,
35
+ `import {Link} from '@primer/react';
36
+ <p>bla blah<Link inline>Link level 1</Link></p>;
37
+ `,
38
+ `import {Link} from '@primer/react';
39
+ <><span>something</span><Link inline={true}>Link level 1</Link></>;
40
+ `,
41
+ `import {Link} from '@primer/react';
42
+ <Link>Link level 1</Link>;
43
+ `,
44
+ `import {Heading, Link} from '@primer/react';
45
+ <Heading>
46
+ <Link>Link level 1</Link>
47
+ hello
48
+ </Heading>
49
+ `,
50
+ `import {Heading, Link} from '@primer/react';
51
+ <Heading as="h2">
52
+ <Link href={somePath}>
53
+ Breadcrumb
54
+ </Link>
55
+ Create a thing
56
+ </Heading>
57
+ `,
58
+ `import {Link} from '@primer/react';
59
+ <div>
60
+ <h2>
61
+ <Link href={somePath}>
62
+ Breadcrumb
63
+ </Link>
64
+ </h2>
65
+ Create a thing
66
+ </div>
67
+ `,
68
+ `import {Link} from '@primer/react';
69
+ <div>
70
+ <Link href={somePath}>
71
+ <GitHubAvatar />{owner}
72
+ </Link>{' '}
73
+ last edited{' '}
74
+ </div>
75
+ `,
76
+ `import {Link} from '@primer/react';
77
+ <span>
78
+ by
79
+ <Link href="something" sx={{p: 2, fontWeight: 'bold'}}>
80
+ Blah blah
81
+ </Link>
82
+ </span>
83
+ `,
84
+ `import {Link} from '@primer/react';
85
+ <span>
86
+ by
87
+ <Link href="something" sx={{fontWeight: 'bold'}}>
88
+ Blah blah
89
+ </Link>
90
+ </span>
91
+ `,
92
+ `import {Link} from '@primer/react';
93
+ <span>
94
+ by
95
+ <Link href="something" sx={{fontFamily: 'mono'}}>
96
+ Blah blah
97
+ </Link>
98
+ </span>
99
+ `,
100
+ `import {Link} from '@primer/react';
101
+ <Box>
102
+
103
+ <Link href="something">
104
+ Blah blah
105
+ </Link>{' '}
106
+ .
107
+ </Box>
108
+ `,
109
+ `import {Link} from '@primer/react';
110
+ <Heading sx={{fontSize: 1, mb: 3}} as="h3">
111
+ In addition,{' '}
112
+ <Link href="https://github.com/pricing" target="_blank">
113
+ GitHub Team
114
+ </Link>{' '}
115
+ includes:
116
+ </Heading>
117
+ `,
118
+ ],
119
+ invalid: [
120
+ {
121
+ code: `import {Link} from '@primer/react';
122
+ <p>bla blah<Link>Link level 1</Link></p>
123
+ `,
124
+ errors: [{messageId: 'linkInTextBlock'}],
125
+ },
126
+ {
127
+ code: `import {Link} from '@primer/react';
128
+ <p><Link>Link level 1</Link> something something</p>
129
+ `,
130
+ errors: [{messageId: 'linkInTextBlock'}],
131
+ },
132
+ {
133
+ code: `import {Link} from '@primer/react';
134
+ <p>bla blah<Link inline={false}>Link level 1</Link></p>
135
+ `,
136
+ errors: [{messageId: 'linkInTextBlock'}],
137
+ },
138
+ {
139
+ code: `import {Link} from '@primer/react';
140
+ <Box>Something something{' '}
141
+ <Link>Link level 1</Link>
142
+ </Box>
143
+ `,
144
+ errors: [{messageId: 'linkInTextBlock'}],
145
+ },
146
+ {
147
+ code: `import {Link} from '@primer/react';
148
+ <>blah blah blah{' '}
149
+ <Link>Link level 1</Link></>;
150
+ `,
151
+ errors: [{messageId: 'linkInTextBlock'}],
152
+ },
153
+ {
154
+ code: `import {Link} from '@primer/react';
155
+ <>blah blah blah{' '}
156
+ <Link underline>Link level 1</Link></>;
157
+ `,
158
+ errors: [{messageId: 'linkInTextBlock'}],
159
+ },
160
+ ],
161
+ })
@@ -46,12 +46,13 @@ module.exports = {
46
46
  },
47
47
  },
48
48
  create(context) {
49
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
49
50
  return {
50
51
  JSXOpeningElement(jsxNode) {
51
52
  const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
52
53
 
53
54
  if (
54
- (skipImportCheck || isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) &&
55
+ (skipImportCheck || isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) &&
55
56
  isHeadingComponent(jsxNode)
56
57
  ) {
57
58
  const error = isInvalid(jsxNode)
@@ -0,0 +1,104 @@
1
+ const {isPrimerComponent} = require('../utils/is-primer-component')
2
+ const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
3
+ const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
4
+
5
+ module.exports = {
6
+ meta: {
7
+ type: 'problem',
8
+ schema: [
9
+ {
10
+ properties: {
11
+ skipImportCheck: {
12
+ type: 'boolean',
13
+ },
14
+ },
15
+ },
16
+ ],
17
+ messages: {
18
+ linkInTextBlock: '<Link>s that are used within a text block should have the inline prop.',
19
+ },
20
+ },
21
+ create(context) {
22
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
23
+ return {
24
+ JSXElement(node) {
25
+ const name = getJSXOpeningElementName(node.openingElement)
26
+ if (
27
+ isPrimerComponent(node.openingElement.name, sourceCode.getScope(node)) &&
28
+ name === 'Link' &&
29
+ node.parent.children
30
+ ) {
31
+ let siblings = node.parent.children
32
+ const parentName = node.parent.openingElement?.name?.name
33
+ // Skip if Link is nested inside of a heading.
34
+ const parentsToSkip = ['Heading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
35
+ if (parentsToSkip.includes(parentName)) return
36
+ if (siblings.length > 0) {
37
+ siblings = siblings.filter(childNode => {
38
+ return (
39
+ !(childNode.type === 'JSXText' && /^\s+$/.test(childNode.value)) &&
40
+ !(
41
+ childNode.type === 'JSXExpressionContainer' &&
42
+ childNode.expression.type === 'Literal' &&
43
+ /^\s+$/.test(childNode.expression.value)
44
+ ) &&
45
+ !(childNode.type === 'Literal' && /^\s+$/.test(childNode.value))
46
+ )
47
+ })
48
+ const index = siblings.findIndex(childNode => {
49
+ return childNode.range === node.range
50
+ })
51
+ const prevSibling = siblings[index - 1]
52
+ const nextSibling = siblings[index + 1]
53
+
54
+ const prevSiblingIsText = prevSibling && prevSibling.type === 'JSXText'
55
+ const nextSiblingIsText = nextSibling && nextSibling.type === 'JSXText'
56
+ if (prevSiblingIsText || nextSiblingIsText) {
57
+ // Skip if the only text adjacent to the link is a period, then skip it.
58
+ if (!prevSiblingIsText && /^\s*\.+\s*$/.test(nextSibling.value)) {
59
+ return
60
+ }
61
+ const sxAttribute = getJSXOpeningElementAttribute(node.openingElement, 'sx')
62
+ const inlineAttribute = getJSXOpeningElementAttribute(node.openingElement, 'inline')
63
+
64
+ // Skip if Link child is a JSX element.
65
+ const jsxElementChildren = node.children.filter(child => {
66
+ return child.type === 'JSXElement'
67
+ })
68
+ if (jsxElementChildren.length > 0) return
69
+
70
+ // Skip if fontWeight or fontFamily is set via the sx prop since these may technically be considered sufficiently distinguishing styles that don't use color.
71
+ if (
72
+ sxAttribute &&
73
+ sxAttribute?.value?.expression &&
74
+ sxAttribute.value.expression.type === 'ObjectExpression' &&
75
+ sxAttribute.value.expression.properties &&
76
+ sxAttribute.value.expression.properties.length > 0
77
+ ) {
78
+ const fontStyleProperty = sxAttribute.value.expression.properties.filter(property => {
79
+ return property.key.name === 'fontWeight' || property.key.name === 'fontFamily'
80
+ })
81
+ if (fontStyleProperty.length > 0) return
82
+ }
83
+ if (inlineAttribute) {
84
+ if (!inlineAttribute.value) {
85
+ return
86
+ } else if (inlineAttribute.value.type === 'JSXExpressionContainer') {
87
+ if (inlineAttribute.value.expression.type === 'Literal') {
88
+ if (inlineAttribute.value.expression.value === true) {
89
+ return
90
+ }
91
+ }
92
+ }
93
+ }
94
+ context.report({
95
+ node,
96
+ messageId: 'linkInTextBlock',
97
+ })
98
+ }
99
+ }
100
+ }
101
+ },
102
+ }
103
+ },
104
+ }
@@ -152,13 +152,14 @@ module.exports = {
152
152
  },
153
153
  },
154
154
  create(context) {
155
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
155
156
  return {
156
157
  JSXElement(jsxNode) {
157
158
  // If `skipImportCheck` is true, this rule will check for non-interactive element in any components (not just ones that are imported from `@primer/react`).
158
159
  const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
159
160
  const name = getJSXOpeningElementName(jsxNode.openingElement)
160
161
  if (
161
- (skipImportCheck || isPrimerComponent(jsxNode.openingElement.name, context.getScope(jsxNode))) &&
162
+ (skipImportCheck || isPrimerComponent(jsxNode.openingElement.name, sourceCode.getScope(jsxNode))) &&
162
163
  name === 'Tooltip' &&
163
164
  jsxNode.children
164
165
  ) {
@@ -45,6 +45,7 @@ module.exports = {
45
45
  },
46
46
  create(context) {
47
47
  const stack = []
48
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
48
49
  return {
49
50
  JSXOpeningElement(jsxNode) {
50
51
  const name = getJSXOpeningElementName(jsxNode)
@@ -56,7 +57,7 @@ module.exports = {
56
57
  // If component is a Primer component and a slot child,
57
58
  // check if it's a direct child of the slot parent
58
59
  if (
59
- (skipImportCheck || isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) &&
60
+ (skipImportCheck || isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) &&
60
61
  slotChildToParentMap[name]
61
62
  ) {
62
63
  const expectedParentNames = slotChildToParentMap[name]
@@ -78,6 +78,8 @@ module.exports = {
78
78
  ...(includeUtilityComponents ? [] : utilityComponents),
79
79
  ])
80
80
 
81
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
82
+
81
83
  return {
82
84
  JSXOpeningElement(jsxNode) {
83
85
  if (skipImportCheck) {
@@ -86,7 +88,7 @@ module.exports = {
86
88
  if (isHTMLElement(jsxNode)) return
87
89
  } else {
88
90
  // skip if component is not imported from primer/react
89
- if (!isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) return
91
+ if (!isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) return
90
92
  }
91
93
 
92
94
  const componentName = getJSXOpeningElementName(jsxNode)