Skip to content

Commit 7eb9d64

Browse files
committed
chore: update eslint-plugin
1 parent 1aeea5f commit 7eb9d64

35 files changed

Lines changed: 2295 additions & 43 deletions

packages/eslint-plugin/README.md

Lines changed: 465 additions & 0 deletions
Large diffs are not rendered by default.

packages/eslint-plugin/src/index.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
1+
import noNestedAccordion from './rules/accordion/no-nested-accordion.js';
2+
import badgeCornerPlacementRules from './rules/badge/badge-corner-placement-rules.js';
3+
import badgeNoInlineInInteractive from './rules/badge/badge-no-inline-in-interactive.js';
14
import buttonNoTextRequiresTooltip from './rules/button/button-no-text-requires-tooltip.js';
5+
import buttonSingleIconAttribute from './rules/button/button-single-icon-attribute.js';
26
import buttonTypeRequired from './rules/button/button-type-required.js';
7+
import closeButtonTextRequired from './rules/close-button/close-button-text-required.js';
38
import textOrChildrenRequired from './rules/content/text-or-children-required.js';
49
import formLabelRequired from './rules/form/form-label-required.js';
10+
import formValidationMessageRequired from './rules/form/form-validation-message-required.js';
11+
import headerBurgerMenuLabelRequired from './rules/header/header-burger-menu-label-required.js';
512
import preferIconAttribute from './rules/icon/prefer-icon-attribute.js';
13+
import inputFileTypeValidation from './rules/input/input-file-type-validation.js';
14+
import inputTypeRequired from './rules/input/input-type-required.js';
15+
import linkExternalSecurity from './rules/link/link-external-security.js';
16+
import navigationItemBackButtonTextRequired from './rules/navigation/navigation-item-back-button-text-required.js';
17+
import customSelectTagsRemoveTextRequired from './rules/select/custom-select-tags-remove-text-required.js';
18+
import selectRequiresOptions from './rules/select/select-requires-options.js';
19+
import tagRemovableRemoveButtonRequired from './rules/tag/tag-removable-remove-button-required.js';
620
import noInteractiveTooltipContent from './rules/tooltip/no-interactive-tooltip-content.js';
721
import tooltipRequiresInteractiveParent from './rules/tooltip/tooltip-requires-interactive-parent.js';
822

923
const recommended = {
1024
rules: {
1125
'db-ux/button-no-text-requires-tooltip': 'error',
1226
'db-ux/button-type-required': 'error',
27+
'db-ux/button-single-icon-attribute': 'error',
1328
'db-ux/form-label-required': 'error',
29+
'db-ux/form-validation-message-required': 'error',
1430
'db-ux/prefer-icon-attribute': 'warn',
1531
'db-ux/text-or-children-required': 'error',
1632
'db-ux/no-interactive-tooltip-content': 'error',
17-
'db-ux/tooltip-requires-interactive-parent': 'error'
33+
'db-ux/tooltip-requires-interactive-parent': 'error',
34+
'db-ux/no-nested-accordion': 'error',
35+
'db-ux/badge-corner-placement-rules': 'error',
36+
'db-ux/badge-no-inline-in-interactive': 'error',
37+
'db-ux/link-external-security': 'warn',
38+
'db-ux/select-requires-options': 'error',
39+
'db-ux/custom-select-tags-remove-text-required': 'error',
40+
'db-ux/close-button-text-required': 'error',
41+
'db-ux/header-burger-menu-label-required': 'error',
42+
'db-ux/navigation-item-back-button-text-required': 'error',
43+
'db-ux/tag-removable-remove-button-required': 'error',
44+
'db-ux/input-type-required': 'warn',
45+
'db-ux/input-file-type-validation': 'error'
1846
}
1947
};
2048

@@ -26,11 +54,28 @@ const plugin = {
2654
rules: {
2755
'button-no-text-requires-tooltip': buttonNoTextRequiresTooltip,
2856
'button-type-required': buttonTypeRequired,
57+
'button-single-icon-attribute': buttonSingleIconAttribute,
2958
'form-label-required': formLabelRequired,
59+
'form-validation-message-required': formValidationMessageRequired,
3060
'prefer-icon-attribute': preferIconAttribute,
3161
'text-or-children-required': textOrChildrenRequired,
3262
'no-interactive-tooltip-content': noInteractiveTooltipContent,
33-
'tooltip-requires-interactive-parent': tooltipRequiresInteractiveParent
63+
'tooltip-requires-interactive-parent': tooltipRequiresInteractiveParent,
64+
'no-nested-accordion': noNestedAccordion,
65+
'badge-corner-placement-rules': badgeCornerPlacementRules,
66+
'badge-no-inline-in-interactive': badgeNoInlineInInteractive,
67+
'link-external-security': linkExternalSecurity,
68+
'select-requires-options': selectRequiresOptions,
69+
'custom-select-tags-remove-text-required':
70+
customSelectTagsRemoveTextRequired,
71+
'close-button-text-required': closeButtonTextRequired,
72+
'header-burger-menu-label-required': headerBurgerMenuLabelRequired,
73+
'navigation-item-back-button-text-required':
74+
navigationItemBackButtonTextRequired,
75+
'tag-removable-remove-button-required':
76+
tagRemovableRemoveButtonRequired,
77+
'input-type-required': inputTypeRequired,
78+
'input-file-type-validation': inputFileTypeValidation
3479
},
3580
configs: {
3681
recommended
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { ESLintUtils } from '@typescript-eslint/utils';
3+
import { isDBComponent } from '../../shared/utils.js';
4+
5+
const createRule = ESLintUtils.RuleCreator(
6+
(name) =>
7+
`https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`
8+
);
9+
10+
export default createRule({
11+
name: 'no-nested-accordion',
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description: 'Prevent nesting DBAccordion components'
16+
},
17+
messages: {
18+
noNested:
19+
'DBAccordion must not be nested inside another DBAccordion as it confuses users'
20+
},
21+
schema: []
22+
},
23+
defaultOptions: [],
24+
create(context) {
25+
return {
26+
JSXElement(node: TSESTree.JSXElement) {
27+
if (!isDBComponent(node.openingElement, 'DBAccordion')) return;
28+
29+
let parent: TSESTree.Node | undefined = node.parent;
30+
while (parent) {
31+
if (
32+
parent.type === 'JSXElement' &&
33+
isDBComponent(parent.openingElement, 'DBAccordion')
34+
) {
35+
context.report({
36+
node: node.openingElement,
37+
messageId: 'noNested'
38+
});
39+
return;
40+
}
41+
parent = parent.parent;
42+
}
43+
}
44+
};
45+
}
46+
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { ESLintUtils } from '@typescript-eslint/utils';
3+
import { getAttributeValue, isDBComponent } from '../../shared/utils.js';
4+
5+
const createRule = ESLintUtils.RuleCreator(
6+
(name) =>
7+
`https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`
8+
);
9+
10+
function getTextContent(node: TSESTree.JSXElement): string | null {
11+
const textChild = node.children.find((child) => child.type === 'JSXText');
12+
return textChild && textChild.type === 'JSXText'
13+
? textChild.value.trim()
14+
: null;
15+
}
16+
17+
export default createRule({
18+
name: 'badge-corner-placement-rules',
19+
meta: {
20+
type: 'problem',
21+
docs: {
22+
description:
23+
'Ensure DBBadge with corner placement has max 3 characters and label'
24+
},
25+
fixable: 'code',
26+
messages: {
27+
textTooLong:
28+
'DBBadge with corner placement must have max 3 characters in text/children',
29+
missingLabel:
30+
'DBBadge with corner placement must have a label attribute for accessibility'
31+
},
32+
schema: []
33+
},
34+
defaultOptions: [],
35+
create(context) {
36+
return {
37+
JSXElement(node: TSESTree.JSXElement) {
38+
if (!isDBComponent(node.openingElement, 'DBBadge')) return;
39+
40+
const placement = getAttributeValue(
41+
node.openingElement,
42+
'placement'
43+
);
44+
if (!placement || placement === 'inline') return;
45+
46+
const text = getAttributeValue(node.openingElement, 'text');
47+
const children = getTextContent(node);
48+
const content =
49+
(typeof text === 'string' ? text : children) || '';
50+
const label = getAttributeValue(node.openingElement, 'label');
51+
52+
if (content.length > 3) {
53+
context.report({
54+
node: node.openingElement,
55+
messageId: 'textTooLong',
56+
fix(fixer) {
57+
const fixes = [];
58+
const shortText = content.slice(0, 3);
59+
60+
if (text && typeof text === 'string') {
61+
const textAttr =
62+
node.openingElement.attributes.find(
63+
(a) =>
64+
a.type === 'JSXAttribute' &&
65+
a.name.name === 'text'
66+
) as TSESTree.JSXAttribute;
67+
if (textAttr) {
68+
fixes.push(
69+
fixer.replaceText(
70+
textAttr,
71+
`text="${shortText}" label="${content}"`
72+
)
73+
);
74+
}
75+
} else if (children) {
76+
const textChild = node.children.find(
77+
(c) => c.type === 'JSXText'
78+
) as TSESTree.JSXText;
79+
if (textChild) {
80+
fixes.push(
81+
fixer.replaceText(textChild, shortText)
82+
);
83+
const lastAttr =
84+
node.openingElement.attributes[
85+
node.openingElement.attributes
86+
.length - 1
87+
];
88+
const insertPos = lastAttr
89+
? lastAttr.range[1]
90+
: node.openingElement.name.range[1];
91+
fixes.push(
92+
fixer.insertTextAfterRange(
93+
[insertPos, insertPos],
94+
` label="${content}"`
95+
)
96+
);
97+
}
98+
}
99+
100+
return fixes;
101+
}
102+
});
103+
}
104+
105+
if (!label) {
106+
context.report({
107+
node: node.openingElement,
108+
messageId: 'missingLabel'
109+
});
110+
}
111+
}
112+
};
113+
}
114+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { ESLintUtils } from '@typescript-eslint/utils';
3+
import { getAttributeValue, isDBComponent } from '../../shared/utils.js';
4+
5+
const createRule = ESLintUtils.RuleCreator(
6+
(name) =>
7+
`https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`
8+
);
9+
10+
const INTERACTIVE_PARENTS = ['DBButton', 'DBLink', 'button', 'a'];
11+
12+
export default createRule({
13+
name: 'badge-no-inline-in-interactive',
14+
meta: {
15+
type: 'problem',
16+
docs: {
17+
description:
18+
'Prevent inline placement for DBBadge inside interactive elements'
19+
},
20+
fixable: 'code',
21+
messages: {
22+
noInline:
23+
'DBBadge inside {{parent}} cannot have placement="inline". Use corner placement instead'
24+
},
25+
schema: []
26+
},
27+
defaultOptions: [],
28+
create(context) {
29+
return {
30+
JSXElement(node: TSESTree.JSXElement) {
31+
if (!isDBComponent(node.openingElement, 'DBBadge')) return;
32+
33+
const placement = getAttributeValue(
34+
node.openingElement,
35+
'placement'
36+
);
37+
if (placement && placement !== 'inline') return;
38+
39+
let parent: TSESTree.Node | undefined = node.parent;
40+
while (parent) {
41+
if (parent.type === 'JSXElement') {
42+
const parentName = parent.openingElement.name;
43+
if (parentName.type === 'JSXIdentifier') {
44+
const name = parentName.name;
45+
const matchedParent = INTERACTIVE_PARENTS.find(
46+
(p) =>
47+
name === p ||
48+
name ===
49+
p.toLowerCase().replace('db', 'db-')
50+
);
51+
52+
if (matchedParent) {
53+
context.report({
54+
node: node.openingElement,
55+
messageId: 'noInline',
56+
data: { parent: matchedParent },
57+
fix(fixer) {
58+
const placementAttr =
59+
node.openingElement.attributes.find(
60+
(a) =>
61+
a.type === 'JSXAttribute' &&
62+
a.name.name === 'placement'
63+
) as TSESTree.JSXAttribute;
64+
65+
if (placementAttr) {
66+
return fixer.replaceText(
67+
placementAttr,
68+
'placement="corner-top-right"'
69+
);
70+
} else {
71+
const lastAttr =
72+
node.openingElement.attributes[
73+
node.openingElement
74+
.attributes.length - 1
75+
];
76+
const insertPos = lastAttr
77+
? lastAttr.range[1]
78+
: node.openingElement.name
79+
.range[1];
80+
return fixer.insertTextAfterRange(
81+
[insertPos, insertPos],
82+
' placement="corner-top-right"'
83+
);
84+
}
85+
}
86+
});
87+
return;
88+
}
89+
}
90+
}
91+
parent = parent.parent;
92+
}
93+
}
94+
};
95+
}
96+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { ESLintUtils } from '@typescript-eslint/utils';
3+
import { getAttributeValue, isDBComponent } from '../../shared/utils.js';
4+
5+
const createRule = ESLintUtils.RuleCreator(
6+
(name) =>
7+
`https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#${name}`
8+
);
9+
10+
export default createRule({
11+
name: 'button-single-icon-attribute',
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description: 'Ensure DBButton uses only one icon attribute'
16+
},
17+
messages: {
18+
multipleIcons:
19+
'DBButton can only use one of: icon, iconLeading, or iconTrailing'
20+
},
21+
schema: []
22+
},
23+
defaultOptions: [],
24+
create(context) {
25+
return {
26+
JSXElement(node: TSESTree.JSXElement) {
27+
if (!isDBComponent(node.openingElement, 'DBButton')) return;
28+
29+
const icon = getAttributeValue(node.openingElement, 'icon');
30+
const iconLeading = getAttributeValue(
31+
node.openingElement,
32+
'iconLeading'
33+
);
34+
const iconTrailing = getAttributeValue(
35+
node.openingElement,
36+
'iconTrailing'
37+
);
38+
39+
const iconCount = [icon, iconLeading, iconTrailing].filter(
40+
Boolean
41+
).length;
42+
43+
if (iconCount > 1) {
44+
context.report({
45+
node: node.openingElement,
46+
messageId: 'multipleIcons'
47+
});
48+
}
49+
}
50+
};
51+
}
52+
});

0 commit comments

Comments
 (0)