@db-ux/core-eslint-plugin 0.0.0 → 4.4.2-eslint-plugin2-696cb23

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +843 -0
  3. package/build/index.d.ts +352 -0
  4. package/build/index.js +82 -0
  5. package/build/rules/accordion/accordion-item-headline-required.d.ts +16 -0
  6. package/build/rules/accordion/accordion-item-headline-required.js +61 -0
  7. package/build/rules/accordion/no-nested-accordion.d.ts +16 -0
  8. package/build/rules/accordion/no-nested-accordion.js +54 -0
  9. package/build/rules/badge/badge-corner-placement-rules.d.ts +18 -0
  10. package/build/rules/badge/badge-corner-placement-rules.js +114 -0
  11. package/build/rules/badge/badge-no-inline-in-interactive.d.ts +17 -0
  12. package/build/rules/badge/badge-no-inline-in-interactive.js +129 -0
  13. package/build/rules/button/button-no-text-requires-tooltip.d.ts +18 -0
  14. package/build/rules/button/button-no-text-requires-tooltip.js +109 -0
  15. package/build/rules/button/button-single-icon-attribute.d.ts +16 -0
  16. package/build/rules/button/button-single-icon-attribute.js +49 -0
  17. package/build/rules/button/button-type-required.d.ts +17 -0
  18. package/build/rules/button/button-type-required.js +58 -0
  19. package/build/rules/close-button/close-button-text-required.d.ts +16 -0
  20. package/build/rules/close-button/close-button-text-required.js +38 -0
  21. package/build/rules/content/text-or-children-required.d.ts +16 -0
  22. package/build/rules/content/text-or-children-required.js +47 -0
  23. package/build/rules/form/form-label-required.d.ts +16 -0
  24. package/build/rules/form/form-label-required.js +45 -0
  25. package/build/rules/form/form-validation-message-required.d.ts +16 -0
  26. package/build/rules/form/form-validation-message-required.js +91 -0
  27. package/build/rules/header/header-burger-menu-label-required.d.ts +16 -0
  28. package/build/rules/header/header-burger-menu-label-required.js +43 -0
  29. package/build/rules/icon/prefer-icon-attribute.d.ts +17 -0
  30. package/build/rules/icon/prefer-icon-attribute.js +82 -0
  31. package/build/rules/input/input-file-type-validation.d.ts +18 -0
  32. package/build/rules/input/input-file-type-validation.js +50 -0
  33. package/build/rules/input/input-type-required.d.ts +17 -0
  34. package/build/rules/input/input-type-required.js +54 -0
  35. package/build/rules/link/link-external-security.d.ts +19 -0
  36. package/build/rules/link/link-external-security.js +166 -0
  37. package/build/rules/navigation/navigation-item-back-button-text-required.d.ts +16 -0
  38. package/build/rules/navigation/navigation-item-back-button-text-required.js +43 -0
  39. package/build/rules/select/custom-select-tags-remove-text-required.d.ts +16 -0
  40. package/build/rules/select/custom-select-tags-remove-text-required.js +33 -0
  41. package/build/rules/select/select-requires-options.d.ts +16 -0
  42. package/build/rules/select/select-requires-options.js +45 -0
  43. package/build/rules/tag/tag-removable-remove-button-required.d.ts +16 -0
  44. package/build/rules/tag/tag-removable-remove-button-required.js +49 -0
  45. package/build/rules/tooltip/no-interactive-tooltip-content.d.ts +15 -0
  46. package/build/rules/tooltip/no-interactive-tooltip-content.js +49 -0
  47. package/build/rules/tooltip/tooltip-requires-interactive-parent.d.ts +15 -0
  48. package/build/rules/tooltip/tooltip-requires-interactive-parent.js +47 -0
  49. package/build/shared/constants.d.ts +58 -0
  50. package/build/shared/constants.js +98 -0
  51. package/build/shared/utils.d.ts +54 -0
  52. package/build/shared/utils.js +178 -0
  53. package/package.json +37 -1
@@ -0,0 +1,16 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ messages: {
10
+ [MESSAGE_IDS.FORM_LABEL_REQUIRED]: string;
11
+ };
12
+ schema: never[];
13
+ };
14
+ create(context: any): any;
15
+ };
16
+ export default _default;
@@ -0,0 +1,45 @@
1
+ import { defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
2
+ import { MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
3
+ const FORM_COMPONENTS = [
4
+ 'DBInput',
5
+ 'DBTextarea',
6
+ 'DBSelect',
7
+ 'DBCustomSelect',
8
+ 'DBCheckbox',
9
+ 'DBRadio',
10
+ 'DBSwitch'
11
+ ];
12
+ const COMPONENTS_WITH_CHILDREN_LABEL = ['DBCheckbox', 'DBRadio', 'DBSwitch'];
13
+ export default {
14
+ meta: {
15
+ type: 'problem',
16
+ docs: {
17
+ description: 'Ensure form components have a label attribute for accessibility',
18
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#form-label-required'
19
+ },
20
+ messages: {
21
+ [MESSAGE_IDS.FORM_LABEL_REQUIRED]: MESSAGES.FORM_LABEL_REQUIRED
22
+ },
23
+ schema: []
24
+ },
25
+ create(context) {
26
+ const checkFormComponent = (node) => {
27
+ const openingElement = node.openingElement || node;
28
+ const component = FORM_COMPONENTS.find((comp) => isDBComponent(openingElement, comp));
29
+ if (!component)
30
+ return;
31
+ const label = getAttributeValue(openingElement, 'label');
32
+ const hasChildren = node.children?.some((child) => (child.type === 'JSXText' && child.value.trim() !== '') ||
33
+ (child.type === 'VText' && child.value.trim() !== ''));
34
+ const canUseChildren = COMPONENTS_WITH_CHILDREN_LABEL.includes(component);
35
+ if (!label && !(canUseChildren && hasChildren)) {
36
+ context.report({
37
+ node: openingElement,
38
+ messageId: MESSAGE_IDS.FORM_LABEL_REQUIRED,
39
+ data: { component }
40
+ });
41
+ }
42
+ };
43
+ return defineTemplateBodyVisitor(context, { VElement: checkFormComponent, Element: checkFormComponent }, { JSXElement: checkFormComponent });
44
+ }
45
+ };
@@ -0,0 +1,16 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ messages: {
10
+ [MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED]: string;
11
+ };
12
+ schema: never[];
13
+ };
14
+ create(context: any): any;
15
+ };
16
+ export default _default;
@@ -0,0 +1,91 @@
1
+ import { defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
2
+ import { MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
3
+ const FORM_COMPONENTS = [
4
+ 'DBInput',
5
+ 'DBTextarea',
6
+ 'DBSelect',
7
+ 'DBCustomSelect',
8
+ 'DBCheckbox'
9
+ ];
10
+ export default {
11
+ meta: {
12
+ type: 'suggestion',
13
+ docs: {
14
+ description: 'Ensure form components with validation have invalidMessage',
15
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#form-validation-message-required'
16
+ },
17
+ messages: {
18
+ [MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED]: MESSAGES.FORM_VALIDATION_MESSAGE_REQUIRED
19
+ },
20
+ schema: []
21
+ },
22
+ create(context) {
23
+ const checkFormComponent = (node) => {
24
+ const openingElement = node.openingElement || node;
25
+ const component = FORM_COMPONENTS.find((comp) => isDBComponent(openingElement, comp));
26
+ if (!component)
27
+ return;
28
+ const invalidMessage = getAttributeValue(openingElement, 'invalidMessage');
29
+ if (invalidMessage)
30
+ return;
31
+ const required = getAttributeValue(openingElement, 'required');
32
+ if (required) {
33
+ context.report({
34
+ node: openingElement,
35
+ messageId: MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED,
36
+ data: { component, attribute: 'required' }
37
+ });
38
+ return;
39
+ }
40
+ if (component === 'DBInput' || component === 'DBTextarea') {
41
+ const maxLength = getAttributeValue(openingElement, 'maxLength');
42
+ const minLength = getAttributeValue(openingElement, 'minLength');
43
+ if (maxLength) {
44
+ context.report({
45
+ node: openingElement,
46
+ messageId: MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED,
47
+ data: { component, attribute: 'maxLength' }
48
+ });
49
+ return;
50
+ }
51
+ if (minLength) {
52
+ context.report({
53
+ node: openingElement,
54
+ messageId: MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED,
55
+ data: { component, attribute: 'minLength' }
56
+ });
57
+ return;
58
+ }
59
+ }
60
+ if (component === 'DBInput') {
61
+ const min = getAttributeValue(openingElement, 'min');
62
+ const max = getAttributeValue(openingElement, 'max');
63
+ const pattern = getAttributeValue(openingElement, 'pattern');
64
+ if (min) {
65
+ context.report({
66
+ node: openingElement,
67
+ messageId: MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED,
68
+ data: { component, attribute: 'min' }
69
+ });
70
+ return;
71
+ }
72
+ if (max) {
73
+ context.report({
74
+ node: openingElement,
75
+ messageId: MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED,
76
+ data: { component, attribute: 'max' }
77
+ });
78
+ return;
79
+ }
80
+ if (pattern) {
81
+ context.report({
82
+ node: openingElement,
83
+ messageId: MESSAGE_IDS.FORM_VALIDATION_MESSAGE_REQUIRED,
84
+ data: { component, attribute: 'pattern' }
85
+ });
86
+ }
87
+ }
88
+ };
89
+ return defineTemplateBodyVisitor(context, { VElement: checkFormComponent, Element: checkFormComponent }, { JSXElement: checkFormComponent });
90
+ }
91
+ };
@@ -0,0 +1,16 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ messages: {
10
+ [MESSAGE_IDS.HEADER_MISSING_BURGER_MENU_LABEL]: string;
11
+ };
12
+ schema: never[];
13
+ };
14
+ create(context: any): any;
15
+ };
16
+ export default _default;
@@ -0,0 +1,43 @@
1
+ import { createAngularVisitors, defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
2
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Ensure DBHeader has burgerMenuLabel for accessibility',
8
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#header-burger-menu-label-required'
9
+ },
10
+ messages: {
11
+ [MESSAGE_IDS.HEADER_MISSING_BURGER_MENU_LABEL]: MESSAGES.HEADER_MISSING_BURGER_MENU_LABEL
12
+ },
13
+ schema: []
14
+ },
15
+ create(context) {
16
+ const angularHandler = (node, parserServices) => {
17
+ const burgerMenuLabel = getAttributeValue(node, 'burgerMenuLabel');
18
+ if (!burgerMenuLabel) {
19
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
20
+ context.report({
21
+ loc,
22
+ messageId: MESSAGE_IDS.HEADER_MISSING_BURGER_MENU_LABEL
23
+ });
24
+ }
25
+ };
26
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBHeader, angularHandler);
27
+ if (angularVisitors)
28
+ return angularVisitors;
29
+ const checkHeader = (node) => {
30
+ const openingElement = node.openingElement || node;
31
+ if (!isDBComponent(openingElement, COMPONENTS.DBHeader))
32
+ return;
33
+ const burgerMenuLabel = getAttributeValue(openingElement, 'burgerMenuLabel');
34
+ if (!burgerMenuLabel) {
35
+ context.report({
36
+ node: openingElement,
37
+ messageId: MESSAGE_IDS.HEADER_MISSING_BURGER_MENU_LABEL
38
+ });
39
+ }
40
+ };
41
+ return defineTemplateBodyVisitor(context, { VElement: checkHeader, Element: checkHeader }, { JSXElement: checkHeader });
42
+ }
43
+ };
@@ -0,0 +1,17 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ fixable: string;
10
+ messages: {
11
+ [MESSAGE_IDS.ICON_PREFER_ATTRIBUTE]: string;
12
+ };
13
+ schema: never[];
14
+ };
15
+ create(context: any): any;
16
+ };
17
+ export default _default;
@@ -0,0 +1,82 @@
1
+ import { MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
2
+ import { defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
3
+ const COMPONENTS_WITH_ICON_ATTR = [
4
+ 'DBInput',
5
+ 'DBBrand',
6
+ 'DBButton',
7
+ 'DBCustomSelectListItem',
8
+ 'DBCustomSelect',
9
+ 'DBInfotext',
10
+ 'DBLink',
11
+ 'DBNavigationItem',
12
+ 'DBNotification',
13
+ 'DBSelect',
14
+ 'DBSwitch',
15
+ 'DBTabItem',
16
+ 'DBTag'
17
+ ];
18
+ export default {
19
+ meta: {
20
+ type: 'suggestion',
21
+ docs: {
22
+ description: 'Prefer icon attribute over DBIcon child component',
23
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#prefer-icon-attribute'
24
+ },
25
+ fixable: 'code',
26
+ messages: {
27
+ [MESSAGE_IDS.ICON_PREFER_ATTRIBUTE]: MESSAGES.ICON_PREFER_ATTRIBUTE
28
+ },
29
+ schema: []
30
+ },
31
+ create(context) {
32
+ const checkComponent = (node) => {
33
+ const openingElement = node.openingElement || node;
34
+ const component = COMPONENTS_WITH_ICON_ATTR.find((comp) => isDBComponent(openingElement, comp));
35
+ if (!component)
36
+ return;
37
+ const iconChild = node.children?.find((child) => (child.type === 'JSXElement' ||
38
+ child.type === 'VElement') &&
39
+ isDBComponent(child.openingElement || child, 'DBIcon'));
40
+ if (iconChild) {
41
+ const iconChildOpening = iconChild.openingElement || iconChild;
42
+ const iconValue = getAttributeValue(iconChildOpening, 'icon');
43
+ context.report({
44
+ node: iconChild,
45
+ messageId: MESSAGE_IDS.ICON_PREFER_ATTRIBUTE,
46
+ data: { component },
47
+ fix(fixer) {
48
+ if (!iconValue || typeof iconValue !== 'string')
49
+ return null;
50
+ const fixes = [];
51
+ fixes.push(fixer.remove(iconChild));
52
+ if (node.openingElement) {
53
+ // JSX
54
+ const lastAttr = openingElement.attributes[openingElement.attributes.length - 1];
55
+ const insertPos = lastAttr
56
+ ? lastAttr.range[1]
57
+ : openingElement.name.range[1];
58
+ fixes.push(fixer.insertTextAfterRange([insertPos, insertPos], ` icon="${iconValue}"`));
59
+ }
60
+ else {
61
+ // Vue
62
+ const attrs = openingElement.startTag.attributes;
63
+ if (attrs.length > 0) {
64
+ const lastAttr = attrs[attrs.length - 1];
65
+ const insertPos = lastAttr.range[1];
66
+ fixes.push(fixer.insertTextAfterRange([insertPos, insertPos], ` icon="${iconValue}"`));
67
+ }
68
+ else {
69
+ const insertPos = openingElement.startTag.range[0] +
70
+ 1 +
71
+ openingElement.rawName.length;
72
+ fixes.push(fixer.insertTextAfterRange([insertPos, insertPos], ` icon="${iconValue}"`));
73
+ }
74
+ }
75
+ return fixes;
76
+ }
77
+ });
78
+ }
79
+ };
80
+ return defineTemplateBodyVisitor(context, { VElement: checkComponent, Element: checkComponent }, { JSXElement: checkComponent });
81
+ }
82
+ };
@@ -0,0 +1,18 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ messages: {
10
+ [MESSAGE_IDS.INPUT_FILE_MISSING_ACCEPT]: string;
11
+ [MESSAGE_IDS.INPUT_INVALID_MULTIPLE]: string;
12
+ [MESSAGE_IDS.INPUT_INVALID_ACCEPT]: string;
13
+ };
14
+ schema: never[];
15
+ };
16
+ create(context: any): any;
17
+ };
18
+ export default _default;
@@ -0,0 +1,50 @@
1
+ import { defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
2
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Ensure DBInput file type has accept and validate file-only attributes',
8
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#input-file-type-validation'
9
+ },
10
+ messages: {
11
+ [MESSAGE_IDS.INPUT_FILE_MISSING_ACCEPT]: MESSAGES.INPUT_FILE_MISSING_ACCEPT,
12
+ [MESSAGE_IDS.INPUT_INVALID_MULTIPLE]: MESSAGES.INPUT_INVALID_MULTIPLE,
13
+ [MESSAGE_IDS.INPUT_INVALID_ACCEPT]: MESSAGES.INPUT_INVALID_ACCEPT
14
+ },
15
+ schema: []
16
+ },
17
+ create(context) {
18
+ const checkInput = (node) => {
19
+ const openingElement = node.openingElement || node;
20
+ if (!isDBComponent(openingElement, COMPONENTS.DBInput))
21
+ return;
22
+ const type = getAttributeValue(openingElement, 'type');
23
+ const accept = getAttributeValue(openingElement, 'accept');
24
+ const multiple = getAttributeValue(openingElement, 'multiple');
25
+ if (type === 'file') {
26
+ if (!accept) {
27
+ context.report({
28
+ node: openingElement,
29
+ messageId: MESSAGE_IDS.INPUT_FILE_MISSING_ACCEPT
30
+ });
31
+ }
32
+ }
33
+ else {
34
+ if (multiple) {
35
+ context.report({
36
+ node: openingElement,
37
+ messageId: MESSAGE_IDS.INPUT_INVALID_MULTIPLE
38
+ });
39
+ }
40
+ if (accept) {
41
+ context.report({
42
+ node: openingElement,
43
+ messageId: MESSAGE_IDS.INPUT_INVALID_ACCEPT
44
+ });
45
+ }
46
+ }
47
+ };
48
+ return defineTemplateBodyVisitor(context, { VElement: checkInput, Element: checkInput }, { JSXElement: checkInput });
49
+ }
50
+ };
@@ -0,0 +1,17 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ fixable: string;
10
+ messages: {
11
+ [MESSAGE_IDS.INPUT_TYPE_REQUIRED]: string;
12
+ };
13
+ schema: never[];
14
+ };
15
+ create(context: any): any;
16
+ };
17
+ export default _default;
@@ -0,0 +1,54 @@
1
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
2
+ import { createAngularFix, createAngularVisitors, createJsxVueFix, defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
3
+ export default {
4
+ meta: {
5
+ type: 'suggestion',
6
+ docs: {
7
+ description: 'Ensure DBInput has type attribute for better developer experience',
8
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#input-type-required'
9
+ },
10
+ fixable: 'code',
11
+ messages: {
12
+ [MESSAGE_IDS.INPUT_TYPE_REQUIRED]: MESSAGES.INPUT_TYPE_REQUIRED
13
+ },
14
+ schema: []
15
+ },
16
+ create(context) {
17
+ const angularHandler = (node, parserServices) => {
18
+ const type = getAttributeValue(node, 'type');
19
+ if (!type) {
20
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
21
+ context.report({
22
+ loc,
23
+ messageId: MESSAGE_IDS.INPUT_TYPE_REQUIRED,
24
+ fix(fixer) {
25
+ const fixData = createAngularFix(context, node, ' type="text"');
26
+ if (!fixData)
27
+ return null;
28
+ return fixer.insertTextBeforeRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
29
+ }
30
+ });
31
+ }
32
+ };
33
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBInput, angularHandler);
34
+ if (angularVisitors)
35
+ return angularVisitors;
36
+ const checkInput = (node) => {
37
+ const openingElement = node.openingElement || node;
38
+ if (!isDBComponent(openingElement, COMPONENTS.DBInput))
39
+ return;
40
+ const type = getAttributeValue(openingElement, 'type');
41
+ if (!type) {
42
+ context.report({
43
+ node: openingElement,
44
+ messageId: MESSAGE_IDS.INPUT_TYPE_REQUIRED,
45
+ fix(fixer) {
46
+ const fixData = createJsxVueFix(node, openingElement, ' type="text"');
47
+ return fixer.insertTextAfterRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
48
+ }
49
+ });
50
+ }
51
+ };
52
+ return defineTemplateBodyVisitor(context, { VElement: checkInput, Element: checkInput }, { JSXElement: checkInput });
53
+ }
54
+ };
@@ -0,0 +1,19 @@
1
+ import { MESSAGE_IDS } from '../../shared/constants.js';
2
+ declare const _default: {
3
+ meta: {
4
+ type: string;
5
+ docs: {
6
+ description: string;
7
+ url: string;
8
+ };
9
+ fixable: string;
10
+ messages: {
11
+ [MESSAGE_IDS.LINK_MISSING_TARGET_BLANK]: string;
12
+ [MESSAGE_IDS.LINK_MISSING_REFERRER_POLICY]: string;
13
+ [MESSAGE_IDS.LINK_MISSING_CONTENT_EXTERNAL]: string;
14
+ };
15
+ schema: never[];
16
+ };
17
+ create(context: any): any;
18
+ };
19
+ export default _default;
@@ -0,0 +1,166 @@
1
+ import { COMPONENTS, MESSAGES, MESSAGE_IDS } from '../../shared/constants.js';
2
+ import { createAngularFix, createAngularVisitors, defineTemplateBodyVisitor, getAttributeValue, isDBComponent } from '../../shared/utils.js';
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Ensure external links have proper security attributes',
8
+ url: 'https://github.com/db-ux-design-system/core-web/blob/main/packages/eslint-plugin/README.md#link-external-security'
9
+ },
10
+ fixable: 'code',
11
+ messages: {
12
+ [MESSAGE_IDS.LINK_MISSING_TARGET_BLANK]: MESSAGES.LINK_MISSING_TARGET_BLANK,
13
+ [MESSAGE_IDS.LINK_MISSING_REFERRER_POLICY]: MESSAGES.LINK_MISSING_REFERRER_POLICY,
14
+ [MESSAGE_IDS.LINK_MISSING_CONTENT_EXTERNAL]: MESSAGES.LINK_MISSING_CONTENT_EXTERNAL
15
+ },
16
+ schema: []
17
+ },
18
+ create(context) {
19
+ const angularHandler = (node, parserServices) => {
20
+ const content = getAttributeValue(node, 'content');
21
+ const target = getAttributeValue(node, 'target');
22
+ const referrerPolicy = getAttributeValue(node, 'referrerPolicy');
23
+ const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
24
+ if (content === 'external' && target !== '_blank') {
25
+ context.report({
26
+ loc,
27
+ messageId: MESSAGE_IDS.LINK_MISSING_TARGET_BLANK,
28
+ fix(fixer) {
29
+ const fixData = createAngularFix(context, node, ' target="_blank"');
30
+ if (!fixData)
31
+ return null;
32
+ return fixer.insertTextBeforeRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
33
+ }
34
+ });
35
+ }
36
+ if (content === 'external' && !referrerPolicy) {
37
+ context.report({
38
+ loc,
39
+ messageId: MESSAGE_IDS.LINK_MISSING_REFERRER_POLICY,
40
+ fix(fixer) {
41
+ const fixData = createAngularFix(context, node, ' referrerPolicy="no-referrer"');
42
+ if (!fixData)
43
+ return null;
44
+ return fixer.insertTextBeforeRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
45
+ }
46
+ });
47
+ }
48
+ if (target === '_blank' && content !== 'external') {
49
+ context.report({
50
+ loc,
51
+ messageId: MESSAGE_IDS.LINK_MISSING_CONTENT_EXTERNAL,
52
+ fix(fixer) {
53
+ const fixData = createAngularFix(context, node, ' content="external"');
54
+ if (!fixData)
55
+ return null;
56
+ return fixer.insertTextBeforeRange([fixData.insertPos, fixData.insertPos], fixData.attributeText);
57
+ }
58
+ });
59
+ }
60
+ };
61
+ const angularVisitors = createAngularVisitors(context, COMPONENTS.DBLink, angularHandler);
62
+ if (angularVisitors)
63
+ return angularVisitors;
64
+ const checkLink = (node) => {
65
+ const openingElement = node.openingElement || node;
66
+ if (!isDBComponent(openingElement, COMPONENTS.DBLink))
67
+ return;
68
+ const content = getAttributeValue(openingElement, 'content');
69
+ const target = getAttributeValue(openingElement, 'target');
70
+ const referrerPolicy = getAttributeValue(openingElement, 'referrerPolicy');
71
+ if (content === 'external') {
72
+ if (target !== '_blank') {
73
+ context.report({
74
+ node: openingElement,
75
+ messageId: MESSAGE_IDS.LINK_MISSING_TARGET_BLANK,
76
+ fix(fixer) {
77
+ if (node.openingElement) {
78
+ const lastAttr = openingElement.attributes[openingElement.attributes.length - 1];
79
+ const insertPos = lastAttr
80
+ ? lastAttr.range[1]
81
+ : openingElement.name.range[1];
82
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' target="_blank"');
83
+ }
84
+ else {
85
+ const attrs = openingElement.startTag.attributes;
86
+ if (attrs.length > 0) {
87
+ return fixer.insertTextAfterRange([
88
+ attrs[attrs.length - 1].range[1],
89
+ attrs[attrs.length - 1].range[1]
90
+ ], ' target="_blank"');
91
+ }
92
+ else {
93
+ const insertPos = openingElement.startTag.range[0] +
94
+ 1 +
95
+ openingElement.rawName.length;
96
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' target="_blank"');
97
+ }
98
+ }
99
+ }
100
+ });
101
+ }
102
+ if (!referrerPolicy) {
103
+ context.report({
104
+ node: openingElement,
105
+ messageId: MESSAGE_IDS.LINK_MISSING_REFERRER_POLICY,
106
+ fix(fixer) {
107
+ if (node.openingElement) {
108
+ const lastAttr = openingElement.attributes[openingElement.attributes.length - 1];
109
+ const insertPos = lastAttr
110
+ ? lastAttr.range[1]
111
+ : openingElement.name.range[1];
112
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' referrerPolicy="no-referrer"');
113
+ }
114
+ else {
115
+ const attrs = openingElement.startTag.attributes;
116
+ if (attrs.length > 0) {
117
+ return fixer.insertTextAfterRange([
118
+ attrs[attrs.length - 1].range[1],
119
+ attrs[attrs.length - 1].range[1]
120
+ ], ' referrerPolicy="no-referrer"');
121
+ }
122
+ else {
123
+ const insertPos = openingElement.startTag.range[0] +
124
+ 1 +
125
+ openingElement.rawName.length;
126
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' referrerPolicy="no-referrer"');
127
+ }
128
+ }
129
+ }
130
+ });
131
+ }
132
+ }
133
+ if (target === '_blank' && content !== 'external') {
134
+ context.report({
135
+ node: openingElement,
136
+ messageId: MESSAGE_IDS.LINK_MISSING_CONTENT_EXTERNAL,
137
+ fix(fixer) {
138
+ if (node.openingElement) {
139
+ const lastAttr = openingElement.attributes[openingElement.attributes.length - 1];
140
+ const insertPos = lastAttr
141
+ ? lastAttr.range[1]
142
+ : openingElement.name.range[1];
143
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' content="external"');
144
+ }
145
+ else {
146
+ const attrs = openingElement.startTag.attributes;
147
+ if (attrs.length > 0) {
148
+ return fixer.insertTextAfterRange([
149
+ attrs[attrs.length - 1].range[1],
150
+ attrs[attrs.length - 1].range[1]
151
+ ], ' content="external"');
152
+ }
153
+ else {
154
+ const insertPos = openingElement.startTag.range[0] +
155
+ 1 +
156
+ openingElement.rawName.length;
157
+ return fixer.insertTextAfterRange([insertPos, insertPos], ' content="external"');
158
+ }
159
+ }
160
+ }
161
+ });
162
+ }
163
+ };
164
+ return defineTemplateBodyVisitor(context, { VElement: checkLink, Element: checkLink }, { JSXElement: checkLink });
165
+ }
166
+ };