eslint-plugin-smarthr 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.3.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.1...v0.3.2) (2023-07-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-heading-in-sectioning-contentを追加 ([#64](https://github.com/kufu/eslint-plugin-smarthr/issues/64)) ([49dc559](https://github.com/kufu/eslint-plugin-smarthr/commit/49dc5591e50ae4b3e496e69faca061c2ca2ca579))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * replacePathsの生成時、tsconfigにcompilerOptions.pathsが設定されていなければエラーになるように修正 ([#63](https://github.com/kufu/eslint-plugin-smarthr/issues/63)) ([cbb4306](https://github.com/kufu/eslint-plugin-smarthr/commit/cbb43061482d5f363c20ca1316aae9e62f2359fa))
16
+
17
+ ### [0.3.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.0...v0.3.1) (2023-05-12)
18
+
5
19
  ## [0.3.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.2.25...v0.3.0) (2023-04-14)
6
20
 
7
21
 
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  - [a11y-anchor-has-href-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-anchor-has-href-attribute)
4
4
  - [a11y-clickable-element-has-text](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-clickable-element-has-text)
5
+ - [a11y-heading-in-sectioning-content](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-heading-in-sectioning-content)
5
6
  - [a11y-image-has-alt-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-image-has-alt-attribute)
6
7
  - [a11y-input-has-name-attribute](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-input-has-name-attribute)
7
8
  - [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
package/libs/common.js CHANGED
@@ -6,13 +6,13 @@ const replacePaths = (() => {
6
6
  const tsconfig = fs.readFileSync(`${process.cwd()}/tsconfig.json`)
7
7
 
8
8
  if (!tsconfig) {
9
- return null
9
+ throw new Error('プロジェクトルートにtsconfig.json を設置してください')
10
10
  }
11
11
 
12
12
  const { compilerOptions } = JSON5.parse(tsconfig)
13
13
 
14
14
  if (!compilerOptions || !compilerOptions.paths) {
15
- return null
15
+ throw new Error('tsconfig.json の compilerOptions.paths に `"@/*": ["any_path/*"]` 形式でフロントエンドのroot dir を指定してください')
16
16
  }
17
17
 
18
18
  const regexp = /\*$/
@@ -23,6 +23,7 @@ const replacePaths = (() => {
23
23
  }
24
24
  }, {})
25
25
  })()
26
+
26
27
  const rootPath = (() => {
27
28
  if (!replacePaths) {
28
29
  return ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -0,0 +1,70 @@
1
+ # smarthr/a11y-heading-in-sectioning-content
2
+
3
+ - Headingコンポーネントをsmarthr-ui/SectioningContent(Article, Aside, Nav, Section, SectioningFragment) のいずれかで囲むことを促すルールです
4
+ - article, aside, nav, section で Heading とHeadingの対象となる範囲を囲むとブラウザが正確に解釈できるようになるメリットがあります
5
+ - またsmarthr-ui/SectioningContentで smarthr-ui/Headingを囲むことで、Headingのレベル(h1~h6)を自動的に計算するメリットもあります
6
+
7
+ ## rules
8
+
9
+ ```js
10
+ {
11
+ rules: {
12
+ 'smarthr/a11y-heading-in-sectioning-content': 'error', // 'warn', 'off'
13
+ },
14
+ }
15
+ ```
16
+
17
+ ## ❌ Incorrect
18
+
19
+ ```jsx
20
+ <div>
21
+ <Heading>
22
+ hoge
23
+ </Heading>
24
+ <Heading>
25
+ fuga
26
+ </Heading>
27
+ </div>
28
+ ```
29
+ ```jsx
30
+ <section> // styled-components の sectionをそのまま利用するとNG
31
+ <Heading>
32
+ hoge
33
+ </Heading>
34
+ </section>
35
+ <section>
36
+ <Heading>
37
+ fuga
38
+ </Heading>
39
+ </section>
40
+ ```
41
+
42
+ ## ✅ Correct
43
+
44
+ ```jsx
45
+ <div>
46
+ <Heading>hoge</Heading> // コンポーネント内にHeadingが一つのみの場合はOK
47
+ </div>
48
+ ```
49
+
50
+ ```jsx
51
+ <Section>
52
+ <Heading>hoge</Heading>
53
+ <Section>
54
+ <Heading>fuga</Heading>
55
+ </Section>
56
+ </Section>
57
+ ```
58
+
59
+ ```jsx
60
+ <Section>
61
+ <Heading>
62
+ hoge
63
+ </Heading>
64
+ </Section>
65
+ <StyledSection>
66
+ <Heading>
67
+ fuga
68
+ </Heading>
69
+ </StyledSection>
70
+ ```
@@ -0,0 +1,112 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
+
3
+ const EXPECTED_NAMES = {
4
+ 'Heading$': 'Heading$',
5
+ '^h(1|2|3|4|5|6)$': 'Heading$',
6
+ 'Article$': 'Article$',
7
+ 'Aside$': 'Aside$',
8
+ 'Nav$': 'Nav$',
9
+ 'Section$': 'Section$',
10
+ }
11
+
12
+ const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
13
+ const sectioningRegex = /((A(rticle|side))|Nav|Section|^SectioningFragment)$/
14
+ const bareTagRegex = /^(article|aside|nav|section)$/
15
+ const messagePrefix = 'Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。'
16
+ const messageSuffix = 'Sectioning Content(Article, Aside, Nav, Section)でHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。現在のマークアップの構造を変更したくない場合はSectioningFragmentコンポーネントを利用してください。'
17
+
18
+ const commonMessage = `${messagePrefix}${messageSuffix}`
19
+ const rootMessage = `${messagePrefix}コンポーネント全体に対するHeadingではない場合、${messageSuffix}コンポーネント全体に対するHeadingの場合、他のHeadingのアウトラインが明確に指定されればエラーにならなくなります。`
20
+
21
+ const reportMessageBareToSHR = (tagName, visibleExample) => {
22
+ const matcher = tagName.match(bareTagRegex)
23
+
24
+ if (matcher) {
25
+ const base = matcher[1]
26
+ const shrComponent = `${base[0].toUpperCase()}${base.slice(1)}`
27
+
28
+ return `"${base}"を利用せず、smarthr-ui/${shrComponent}を拡張してください。Headingのレベルが自動計算されるようになります。${visibleExample ? `(例: "styled.${base}" -> "styled(${shrComponent})")` : ''}`
29
+ }
30
+ }
31
+
32
+ const searchBubbleUp = (node) => {
33
+ if (
34
+ node.type === 'Program' ||
35
+ node.type === 'JSXElement' && node.openingElement.name.name?.match(sectioningRegex)
36
+ ) {
37
+ return node
38
+ }
39
+
40
+ return searchBubbleUp(node.parent)
41
+ }
42
+
43
+ module.exports = {
44
+ meta: {
45
+ type: 'suggestion',
46
+ schema: [],
47
+ },
48
+ create(context) {
49
+ let sections = []
50
+ let { VariableDeclarator, ...formatter } = generateTagFormatter({ context, EXPECTED_NAMES })
51
+
52
+ formatter.VariableDeclarator = (node) => {
53
+ VariableDeclarator(node)
54
+ if (!node.init) {
55
+ return
56
+ }
57
+
58
+ const tag = node.init.tag || node.init
59
+
60
+ if (tag.object?.name === 'styled') {
61
+ const message = reportMessageBareToSHR(tag.property.name, true)
62
+
63
+ if (message) {
64
+ context.report({
65
+ node,
66
+ message,
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ return {
73
+ ...formatter,
74
+ JSXOpeningElement: (node) => {
75
+ const elementName = node.name.name || ''
76
+ const message = reportMessageBareToSHR(elementName, false)
77
+
78
+ if (message) {
79
+ context.report({
80
+ node,
81
+ message,
82
+ })
83
+ } else if (elementName.match(headingRegex)) {
84
+ const result = searchBubbleUp(node.parent)
85
+ const saved = sections.find((s) => s[0] === result)
86
+
87
+ // HINT: 最初の1つ目は通知しない()
88
+ if (!saved) {
89
+ sections.push([result, node])
90
+ } else {
91
+ // HINT: 同じファイルで同じSectioningContent or トップノードを持つ場合
92
+ const [section, unreport] = saved
93
+ const targets = unreport ? [unreport, node] : [node]
94
+
95
+ saved[1] = undefined
96
+
97
+ targets.forEach((n) => {
98
+ context.report({
99
+ node: n,
100
+ message:
101
+ section.type === 'Program'
102
+ ? rootMessage
103
+ : commonMessage,
104
+ })
105
+ })
106
+ }
107
+ }
108
+ },
109
+ }
110
+ },
111
+ }
112
+ module.exports.schema = []
@@ -0,0 +1,66 @@
1
+ const rule = require('../rules/a11y-heading-in-sectioning-content');
2
+ const RuleTester = require('eslint').RuleTester;
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 2018,
7
+ ecmaFeatures: {
8
+ experimentalObjectRestSpread: true,
9
+ jsx: true,
10
+ },
11
+ sourceType: 'module',
12
+ },
13
+ });
14
+
15
+ const rootMessage = 'Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。コンポーネント全体に対するHeadingではない場合、Sectioning Content(Article, Aside, Nav, Section)でHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。現在のマークアップの構造を変更したくない場合はSectioningFragmentコンポーネントを利用してください。コンポーネント全体に対するHeadingの場合、他のHeadingのアウトラインが明確に指定されればエラーにならなくなります。'
16
+ const message = 'Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。Sectioning Content(Article, Aside, Nav, Section)でHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。現在のマークアップの構造を変更したくない場合はSectioningFragmentコンポーネントを利用してください。'
17
+
18
+ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
19
+ valid: [
20
+ { code: `import styled from 'styled-components'` },
21
+ { code: 'const HogeHeading = styled.h1``' },
22
+ { code: 'const HogeHeading = styled.h2``' },
23
+ { code: 'const HogeHeading = styled.h3``' },
24
+ { code: 'const HogeHeading = styled.h4``' },
25
+ { code: 'const HogeHeading = styled.h5``' },
26
+ { code: 'const HogeHeading = styled.h6``' },
27
+ { code: 'const FugaHeading = styled(Heading)``' },
28
+ { code: 'const FugaHeading = styled(HogeHeading)``' },
29
+ { code: 'const FugaArticle = styled(HogeArticle)``' },
30
+ { code: 'const FugaAside = styled(HogeAside)``' },
31
+ { code: 'const FugaNav = styled(HogeNav)``' },
32
+ { code: 'const FugaSection = styled(HogeSection)``' },
33
+ { code: '<Heading>hoge</Heading>' },
34
+ { code: '<HogeHeading>hoge</HogeHeading>' },
35
+ { code: '<><Heading>hoge</Heading><Section><Heading>hoge</Heading></Section></>' },
36
+ { code: '<><Heading>hoge</Heading><Aside><Heading>hoge</Heading></Aside></>' },
37
+ { code: '<><Heading>hoge</Heading><Article><Heading>hoge</Heading></Article></>' },
38
+ { code: '<><Heading>hoge</Heading><Nav><Heading>hoge</Heading></Nav></>' },
39
+ { code: '<><Heading>hoge</Heading><SectioningFragment><Heading>hoge</Heading></SectioningFragment></>' },
40
+ ],
41
+ invalid: [
42
+ { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
43
+ { code: 'const Hoge = styled.h1``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
44
+ { code: 'const Hoge = styled.h2``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
45
+ { code: 'const Hoge = styled.h3``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
46
+ { code: 'const Hoge = styled.h4``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
47
+ { code: 'const Hoge = styled.h5``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
48
+ { code: 'const Hoge = styled.h6``', errors: [ { message: `Hogeを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
49
+ { code: 'const Fuga = styled(Heading)``', errors: [ { message: `Fugaを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
50
+ { code: 'const Fuga = styled(HogeHeading)``', errors: [ { message: `Fugaを正規表現 "/Heading$/" がmatchする名称に変更してください` } ] },
51
+ { code: 'const Fuga = styled(HogeArticle)``', errors: [ { message: `Fugaを正規表現 "/Article$/" がmatchする名称に変更してください` } ] },
52
+ { code: 'const Fuga = styled(HogeAside)``', errors: [ { message: `Fugaを正規表現 "/Aside$/" がmatchする名称に変更してください` } ] },
53
+ { code: 'const Fuga = styled(HogeNav)``', errors: [ { message: `Fugaを正規表現 "/Nav$/" がmatchする名称に変更してください` } ] },
54
+ { code: 'const Fuga = styled(HogeSection)``', errors: [ { message: `Fugaを正規表現 "/Section$/" がmatchする名称に変更してください` } ] },
55
+ { code: 'const StyledArticle = styled.article``', errors: [ { message: `"article"を利用せず、smarthr-ui/Articleを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.article" -> "styled(Article)")` } ] },
56
+ { code: 'const StyledAside = styled.aside``', errors: [ { message: `"aside"を利用せず、smarthr-ui/Asideを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.aside" -> "styled(Aside)")` } ] },
57
+ { code: 'const StyledNav = styled.nav``', errors: [ { message: `"nav"を利用せず、smarthr-ui/Navを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.nav" -> "styled(Nav)")` } ] },
58
+ { code: 'const StyledSection = styled.section``', errors: [ { message: `"section"を利用せず、smarthr-ui/Sectionを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.section" -> "styled(Section)")` } ] },
59
+ { code: '<><Heading>hoge</Heading><Heading>hoge</Heading></>', errors: [ { message: rootMessage }, { message: rootMessage } ] },
60
+ { code: '<><Heading>hoge</Heading><Section><Heading>hoge</Heading><Heading>hoge</Heading></Section></>', errors: [ { message }, { message } ] },
61
+ { code: '<article>hoge</article>', errors: [ { message: `"article"を利用せず、smarthr-ui/Articleを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
62
+ { code: '<aside>hoge</aside>', errors: [ { message: `"aside"を利用せず、smarthr-ui/Asideを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
63
+ { code: '<nav>hoge</nav>', errors: [ { message: `"nav"を利用せず、smarthr-ui/Navを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
64
+ { code: '<section>hoge</section>', errors: [ { message: `"section"を利用せず、smarthr-ui/Sectionを拡張してください。Headingのレベルが自動計算されるようになります。` } ] },
65
+ ],
66
+ });