|
| 1 | +const { generateTagFormatter } = require('../../libs/format_styled_components') |
| 2 | + |
| 3 | +const LAYOUT_EXPECTED_NAMES = { |
| 4 | + 'Center$': '(Center)$', |
| 5 | + 'Cluster$': '(Cluster)$', |
| 6 | + 'Reel$': '(Reel)$', |
| 7 | + 'Sidebar$': '(Sidebar)$', |
| 8 | + 'Stack$': '(Stack)$', |
| 9 | + 'Base$': '(Base)$', |
| 10 | + 'BaseColumn$': '(BaseColumn)$', |
| 11 | +} |
| 12 | +const EXPECTED_NAMES = { |
| 13 | + ...LAYOUT_EXPECTED_NAMES, |
| 14 | + 'PageHeading$': '(PageHeading)$', |
| 15 | + 'Heading$': '(Heading)$', |
| 16 | + '^h1$': '(PageHeading)$', |
| 17 | + '^h(|2|3|4|5|6)$': '(Heading)$', |
| 18 | +} |
| 19 | + |
| 20 | +const UNEXPECTED_NAMES = { |
| 21 | + ...LAYOUT_EXPECTED_NAMES, |
| 22 | + '(Heading|^h(1|2|3|4|5|6))$': '(Heading)$', |
| 23 | +} |
| 24 | + |
| 25 | +const layoutRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/ |
| 26 | +const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/ |
| 27 | +const asRegex = /^(as|forwardedAs)$/ |
| 28 | +const formControlRegex = /(FormControl|Fieldset)$/ |
| 29 | + |
| 30 | +const findAsAttr = (a) => a.name?.name.match(asRegex) |
| 31 | + |
| 32 | +const searchBubbleUp = (node) => { |
| 33 | + switch (node.type) { |
| 34 | + case 'Program': |
| 35 | + // rootまで検索した場合は確定でエラーにする |
| 36 | + return null |
| 37 | + case 'JSXElement': { |
| 38 | + const name = node.openingElement.name.name || '' |
| 39 | + |
| 40 | + if (headingRegex.test(name)) { |
| 41 | + return name |
| 42 | + } |
| 43 | + |
| 44 | + break |
| 45 | + } |
| 46 | + case 'JSXAttribute': { |
| 47 | + const name = node.name.name || '' |
| 48 | + |
| 49 | + if (name === 'title' && formControlRegex.test(node.parent.name.name)) { |
| 50 | + return `${node.parent.name.name}のtitle属性` |
| 51 | + } |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + return searchBubbleUp(node.parent) |
| 56 | +} |
| 57 | + |
| 58 | +/** |
| 59 | + * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>} |
| 60 | + */ |
| 61 | +module.exports = { |
| 62 | + meta: { |
| 63 | + type: 'problem', |
| 64 | + schema: [], |
| 65 | + }, |
| 66 | + create(context) { |
| 67 | + return { |
| 68 | + ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }), |
| 69 | + JSXOpeningElement: (node) => { |
| 70 | + const name = node.name.name || '' |
| 71 | + |
| 72 | + if (layoutRegex.test(name) && !node.attributes.some(findAsAttr)) { |
| 73 | + const parentName = searchBubbleUp(node.parent.parent) |
| 74 | + |
| 75 | + if (parentName) { |
| 76 | + context.report({ |
| 77 | + node, |
| 78 | + message: `${name}は${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります |
| 79 | + - smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します |
| 80 | + - ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます |
| 81 | + - span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`, |
| 82 | + }) |
| 83 | + } |
| 84 | + } |
| 85 | + }, |
| 86 | + } |
| 87 | + }, |
| 88 | +} |
| 89 | + |
| 90 | +module.exports.schema = [] |
0 commit comments