@wise/wds-codemods 0.0.1-experimental-6c2101b

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 (62) hide show
  1. package/.changeset/better-impalas-drop.md +5 -0
  2. package/.changeset/config.json +13 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/actions/bootstrap/action.yml +49 -0
  5. package/.github/actions/commitlint/action.yml +27 -0
  6. package/.github/actions/test/action.yml +23 -0
  7. package/.github/workflows/cd-cd.yml +127 -0
  8. package/.github/workflows/renovate.yml +16 -0
  9. package/.husky/commit-msg +1 -0
  10. package/.husky/pre-commit +1 -0
  11. package/.nvmrc +1 -0
  12. package/.prettierignore +1 -0
  13. package/.prettierrc.js +5 -0
  14. package/README.md +184 -0
  15. package/babel.config.js +28 -0
  16. package/codemod-report.md +81 -0
  17. package/commitlint.config.js +3 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +2448 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/transforms/button.d.ts +20 -0
  22. package/dist/transforms/button.js +640 -0
  23. package/dist/transforms/button.js.map +1 -0
  24. package/eslint.config.js +15 -0
  25. package/jest.config.js +9 -0
  26. package/mkdocs.yml +4 -0
  27. package/package.json +68 -0
  28. package/renovate.json +9 -0
  29. package/scripts/build.sh +10 -0
  30. package/src/__tests__/runCodemod.test.ts +109 -0
  31. package/src/index.ts +4 -0
  32. package/src/runCodemod.ts +149 -0
  33. package/src/transforms/button/__tests__/button.test.tsx +175 -0
  34. package/src/transforms/button/button.ts +453 -0
  35. package/src/transforms/helpers/__tests__/createTestTransform.test.ts +27 -0
  36. package/src/transforms/helpers/__tests__/hasImport.test.ts +52 -0
  37. package/src/transforms/helpers/__tests__/iconUtils.test.ts +207 -0
  38. package/src/transforms/helpers/__tests__/jsxElementUtils.test.ts +130 -0
  39. package/src/transforms/helpers/__tests__/jsxReportingUtils.test.ts +265 -0
  40. package/src/transforms/helpers/__tests__/packageValidation.test.ts +45 -0
  41. package/src/transforms/helpers/createTestTransform.ts +59 -0
  42. package/src/transforms/helpers/hasImport.ts +60 -0
  43. package/src/transforms/helpers/iconUtils.ts +87 -0
  44. package/src/transforms/helpers/index.ts +5 -0
  45. package/src/transforms/helpers/jsxElementUtils.ts +67 -0
  46. package/src/transforms/helpers/jsxReportingUtils.ts +224 -0
  47. package/src/transforms/helpers/packageValidation.ts +53 -0
  48. package/src/utils/__tests__/getOptions.test.ts +219 -0
  49. package/src/utils/__tests__/handleError.test.ts +18 -0
  50. package/src/utils/__tests__/hasPackageVersion.test.ts +191 -0
  51. package/src/utils/__tests__/loadTransformModules.test.ts +51 -0
  52. package/src/utils/__tests__/reportManualReview.test.ts +42 -0
  53. package/src/utils/getOptions.ts +78 -0
  54. package/src/utils/handleError.ts +6 -0
  55. package/src/utils/hasPackageVersion.ts +482 -0
  56. package/src/utils/index.ts +4 -0
  57. package/src/utils/loadTransformModules.ts +28 -0
  58. package/src/utils/reportManualReview.ts +17 -0
  59. package/test-button.tsx +230 -0
  60. package/test-file.js +2 -0
  61. package/tsconfig.json +14 -0
  62. package/tsup.config.js +13 -0
@@ -0,0 +1,52 @@
1
+ import jscodeshift from 'jscodeshift';
2
+
3
+ import hasImport from '../hasImport';
4
+
5
+ describe('hasImport', () => {
6
+ function getRoot(source: string) {
7
+ return jscodeshift(source);
8
+ }
9
+
10
+ it('returns true if the named import exists from the given module', () => {
11
+ const source = `import { Button, Card } from '@transferwise/components';`;
12
+ const root = getRoot(source);
13
+ expect(hasImport(root, '@transferwise/components', 'Button', jscodeshift).exists).toBe(true);
14
+ expect(hasImport(root, '@transferwise/components', 'Card', jscodeshift).exists).toBe(true);
15
+ });
16
+
17
+ it('returns false if the named import does not exist from the given module', () => {
18
+ const source = `import { Button } from '@transferwise/components';`;
19
+ const root = getRoot(source);
20
+ expect(hasImport(root, '@transferwise/components', 'Card', jscodeshift).exists).toBe(false);
21
+ });
22
+
23
+ it('returns false if the import is from a different module', () => {
24
+ const source = `import { Button } from 'other-lib';`;
25
+ const root = getRoot(source);
26
+ expect(hasImport(root, '@transferwise/components', 'Button', jscodeshift).exists).toBe(false);
27
+ });
28
+
29
+ it('returns false if there are no imports at all', () => {
30
+ const source = `const a = 1;`;
31
+ const root = getRoot(source);
32
+ expect(hasImport(root, '@transferwise/components', 'Button', jscodeshift).exists).toBe(false);
33
+ });
34
+
35
+ it('returns true for default imports', () => {
36
+ const source = `import Button from '@transferwise/components';`;
37
+ const root = getRoot(source);
38
+ expect(hasImport(root, '@transferwise/components', 'Button', jscodeshift).exists).toBe(true);
39
+ });
40
+
41
+ it('returns true for aliased imports', () => {
42
+ const source = `import { Button as TWButton } from '@transferwise/components';`;
43
+ const root = getRoot(source);
44
+ expect(hasImport(root, '@transferwise/components', 'Button', jscodeshift).exists).toBe(true);
45
+ });
46
+
47
+ it('returns false for namespace imports', () => {
48
+ const source = `import * as TW from '@transferwise/components';`;
49
+ const root = getRoot(source);
50
+ expect(hasImport(root, '@transferwise/components', 'Button', jscodeshift).exists).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,207 @@
1
+ import jscodeshift, {
2
+ type JSXAttribute,
3
+ type JSXElement,
4
+ type JSXExpressionContainer,
5
+ type JSXOpeningElement,
6
+ } from 'jscodeshift';
7
+
8
+ import { processIconChildren } from '..';
9
+
10
+ describe('processIconChildren', () => {
11
+ const j = jscodeshift;
12
+
13
+ function getRoot(source: string) {
14
+ return j(source);
15
+ }
16
+
17
+ function findOpeningElement(root: ReturnType<typeof jscodeshift>) {
18
+ return (root.find(j.JSXOpeningElement).at(0).get() as { node: JSXOpeningElement }).node;
19
+ }
20
+
21
+ it('adds addonStart attribute when icon is first child', () => {
22
+ const source = `
23
+ <Button>
24
+ <IconA />
25
+ <OtherComponent />
26
+ </Button>
27
+ `;
28
+ const root = getRoot(source);
29
+ const openingElement = findOpeningElement(root);
30
+ const iconImports = new Set(['IconA']);
31
+ const children = root
32
+ .find(j.JSXElement)
33
+ .nodes()
34
+ .filter(
35
+ (node) =>
36
+ node.openingElement.name.type === 'JSXIdentifier' &&
37
+ node.openingElement.name.name !== 'Button',
38
+ );
39
+
40
+ const childrenArray: (JSXElement | JSXExpressionContainer | unknown)[] = children;
41
+
42
+ processIconChildren(j, childrenArray, iconImports, openingElement);
43
+
44
+ expect(openingElement.attributes).toHaveLength(1);
45
+ expect((openingElement.attributes![0] as JSXAttribute).name.name).toBe('addonStart');
46
+ const attrValue = (openingElement.attributes![0] as JSXAttribute).value;
47
+ // Check that there is an attribute named 'addonStart' and it contains 'IconA' somewhere in its value stringified
48
+ const addonStartAttr = openingElement.attributes!.find(
49
+ (attr) => (attr as JSXAttribute).name.name === 'addonStart',
50
+ ) as JSXAttribute | undefined;
51
+ expect(addonStartAttr).toBeDefined();
52
+ const attrValueStr = addonStartAttr && JSON.stringify(addonStartAttr.value);
53
+ expect(attrValueStr).toContain('IconA');
54
+ expect(childrenArray).toHaveLength(1);
55
+ const childOpeningElement = (childrenArray[0] as JSXElement).openingElement;
56
+ expect(
57
+ childOpeningElement.name.type === 'JSXIdentifier' ? childOpeningElement.name.name : undefined,
58
+ ).toBe('OtherComponent');
59
+ });
60
+
61
+ it('adds addonEnd attribute when icon is last child', () => {
62
+ const source = `
63
+ <Button>
64
+ <OtherComponent />
65
+ <IconB />
66
+ </Button>
67
+ `;
68
+ const root = getRoot(source);
69
+ const openingElement = findOpeningElement(root);
70
+ const iconImports = new Set(['IconB']);
71
+ const children = root
72
+ .find(j.JSXElement)
73
+ .nodes()
74
+ .filter(
75
+ (node) =>
76
+ node.openingElement.name.type === 'JSXIdentifier' &&
77
+ node.openingElement.name.name !== 'Button',
78
+ );
79
+
80
+ const childrenArray: (JSXElement | JSXExpressionContainer | unknown)[] = children;
81
+
82
+ processIconChildren(j, childrenArray, iconImports, openingElement);
83
+
84
+ expect(openingElement.attributes).toHaveLength(1);
85
+ expect((openingElement.attributes![0] as JSXAttribute).name.name).toBe('addonEnd');
86
+ const attrValueB = (openingElement.attributes![0] as JSXAttribute).value;
87
+ const addonEndAttr = openingElement.attributes!.find(
88
+ (attr) => (attr as JSXAttribute).name.name === 'addonEnd',
89
+ ) as JSXAttribute | undefined;
90
+ expect(addonEndAttr).toBeDefined();
91
+ const attrValueStrB = addonEndAttr && JSON.stringify(addonEndAttr.value);
92
+ expect(attrValueStrB).toContain('IconB');
93
+ expect(childrenArray).toHaveLength(1);
94
+ const childName = (childrenArray[0] as JSXElement).openingElement.name;
95
+ expect(childName.type === 'JSXIdentifier' ? childName.name : undefined).toBe('OtherComponent');
96
+ });
97
+
98
+ it('unwraps JSXExpressionContainer and processes icon child', () => {
99
+ const source = `
100
+ <Button>
101
+ {<IconC />}
102
+ <OtherComponent />
103
+ </Button>
104
+ `;
105
+ const root = getRoot(source);
106
+ const openingElement = findOpeningElement(root);
107
+ const iconImports = new Set(['IconC']);
108
+ const children = root
109
+ .find(j.JSXElement)
110
+ .nodes()
111
+ .filter(
112
+ (node) =>
113
+ node.openingElement.name.type === 'JSXIdentifier' &&
114
+ node.openingElement.name.name !== 'Button',
115
+ );
116
+
117
+ const wrappedIconChild: JSXExpressionContainer = {
118
+ type: 'JSXExpressionContainer',
119
+ expression: children[0],
120
+ };
121
+ const childrenArray: (JSXElement | JSXExpressionContainer | unknown)[] = [
122
+ wrappedIconChild,
123
+ children[1],
124
+ ];
125
+
126
+ processIconChildren(j, childrenArray, iconImports, openingElement);
127
+
128
+ expect(openingElement.attributes).toHaveLength(1);
129
+ expect((openingElement.attributes![0] as JSXAttribute).name.name).toBe('addonStart');
130
+ const attrValue = (openingElement.attributes![0] as JSXAttribute).value;
131
+ const addonStartAttrC = openingElement.attributes!.find(
132
+ (attr) => (attr as JSXAttribute).name.name === 'addonStart',
133
+ ) as JSXAttribute | undefined;
134
+ expect(addonStartAttrC).toBeDefined();
135
+ const attrValueStrC = addonStartAttrC && JSON.stringify(addonStartAttrC.value);
136
+ expect(attrValueStrC).toContain('IconC');
137
+ expect(childrenArray).toHaveLength(1);
138
+ const childName = (childrenArray[0] as JSXElement).openingElement.name;
139
+ expect(childName.type === 'JSXIdentifier' ? childName.name : undefined).toBe('OtherComponent');
140
+ });
141
+
142
+ it('does nothing if no icon child found', () => {
143
+ const source = `
144
+ <Button>
145
+ <Component1 />
146
+ <Component2 />
147
+ </Button>
148
+ `;
149
+ const root = getRoot(source);
150
+ const openingElement = findOpeningElement(root);
151
+ const iconImports = new Set(['IconX']);
152
+ const children = root
153
+ .find(j.JSXElement)
154
+ .nodes()
155
+ .filter(
156
+ (node) =>
157
+ node.openingElement.name.type === 'JSXIdentifier' &&
158
+ node.openingElement.name.name !== 'Button',
159
+ );
160
+
161
+ const childrenArray: (JSXElement | JSXExpressionContainer | unknown)[] = children;
162
+
163
+ processIconChildren(j, childrenArray, iconImports, openingElement);
164
+
165
+ expect(openingElement.attributes).toHaveLength(0);
166
+ expect(childrenArray).toHaveLength(2);
167
+ });
168
+
169
+ it('does nothing if children is undefined', () => {
170
+ const source = `<Button />`;
171
+ const root = getRoot(source);
172
+ const openingElement = findOpeningElement(root);
173
+ const iconImports = new Set(['IconY']);
174
+
175
+ processIconChildren(j, undefined, iconImports, openingElement);
176
+
177
+ expect(openingElement.attributes).toHaveLength(0);
178
+ });
179
+
180
+ it('does nothing if openingElement.attributes is undefined', () => {
181
+ const source = `
182
+ <Button>
183
+ <IconZ />
184
+ </Button>
185
+ `;
186
+ const root = getRoot(source);
187
+ const iconImports = new Set(['IconZ']);
188
+ const children = root
189
+ .find(j.JSXElement)
190
+ .nodes()
191
+ .filter(
192
+ (node) =>
193
+ node.openingElement.name.type === 'JSXIdentifier' &&
194
+ node.openingElement.name.name !== 'Button',
195
+ );
196
+
197
+ const originalOpeningElement = findOpeningElement(root);
198
+ const openingElement = { ...originalOpeningElement, attributes: undefined };
199
+
200
+ const childrenArray: (JSXElement | JSXExpressionContainer | unknown)[] = children;
201
+
202
+ processIconChildren(j, childrenArray, iconImports, openingElement);
203
+
204
+ expect(openingElement.attributes).toBeUndefined();
205
+ expect(childrenArray).toHaveLength(1);
206
+ });
207
+ });
@@ -0,0 +1,130 @@
1
+ import jscodeshift, { type JSXAttribute, type JSXElement, JSXIdentifier } from 'jscodeshift';
2
+
3
+ import {
4
+ addAttributesIfMissing,
5
+ hasAttribute,
6
+ hasAttributeOnElement,
7
+ setNameIfJSXIdentifier,
8
+ } from '../jsxElementUtils';
9
+
10
+ describe('jsxElementUtils', () => {
11
+ function getRoot(source: string) {
12
+ return jscodeshift(source);
13
+ }
14
+
15
+ describe('setNameIfJSXIdentifier', () => {
16
+ it('renames JSXIdentifier element name', () => {
17
+ const source = `<OldName />`;
18
+ const root = getRoot(source);
19
+ const element = (root.findJSXElements('OldName').at(0).get() as { node: JSXElement }).node;
20
+ const result = setNameIfJSXIdentifier(element.openingElement.name, 'NewName');
21
+ expect(result).toMatchObject({ type: 'JSXIdentifier', name: 'NewName' });
22
+ });
23
+
24
+ it('returns the same element if not JSXIdentifier', () => {
25
+ const source = `<A.B />`;
26
+ const root = getRoot(source);
27
+ // Find the first JSXElement (which will be <A.B />)
28
+ const found = root.find(jscodeshift.JSXElement);
29
+ if (found.length === 0) {
30
+ throw new Error('No JSX elements found for A.B');
31
+ }
32
+ const element = (found.at(0).get() as { node: JSXElement }).node;
33
+ const result = setNameIfJSXIdentifier(element.openingElement.name, 'NewName');
34
+ expect(result).toEqual(element.openingElement.name);
35
+ });
36
+
37
+ it('returns undefined if elementName is undefined', () => {
38
+ const result = setNameIfJSXIdentifier(undefined, 'NewName');
39
+ expect(result).toBeUndefined();
40
+ });
41
+ });
42
+
43
+ describe('hasAttribute', () => {
44
+ it('returns true if attribute with given name exists', () => {
45
+ const source = `<div foo bar />`;
46
+ const root = getRoot(source);
47
+ const element = root.findJSXElements('div').nodes()[0];
48
+ expect(hasAttribute(element.openingElement.attributes, 'foo')).toBe(true);
49
+ expect(hasAttribute(element.openingElement.attributes, 'bar')).toBe(true);
50
+ });
51
+
52
+ it('returns false if attribute with given name does not exist', () => {
53
+ const source = `<div foo />`;
54
+ const root = getRoot(source);
55
+ const element = root.findJSXElements('div').nodes()[0];
56
+ expect(hasAttribute(element.openingElement.attributes, 'baz')).toBe(false);
57
+ });
58
+
59
+ it('ignores JSXSpreadAttribute and returns false if no matching JSXAttribute', () => {
60
+ const source = `<div {...props} />`;
61
+ const root = getRoot(source);
62
+ const element = root.findJSXElements('div').nodes()[0];
63
+ expect(hasAttribute(element.openingElement.attributes, 'foo')).toBe(false);
64
+ });
65
+
66
+ it('returns false if attributes is undefined or not array', () => {
67
+ expect(hasAttribute(undefined, 'foo')).toBe(false);
68
+ expect(hasAttribute(null as never, 'foo')).toBe(false);
69
+ expect(hasAttribute([], 'foo')).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('hasAttributeOnElement', () => {
74
+ it('returns true if openingElement has attribute', () => {
75
+ const source = `<div foo />`;
76
+ const root = getRoot(source);
77
+ const element = root.findJSXElements('div').nodes()[0];
78
+ expect(hasAttributeOnElement(element.openingElement, 'foo')).toBe(true);
79
+ });
80
+
81
+ it('returns false if openingElement does not have attribute', () => {
82
+ const source = `<div bar />`;
83
+ const root = getRoot(source);
84
+ const element = root.findJSXElements('div').nodes()[0];
85
+ expect(hasAttributeOnElement(element.openingElement, 'foo')).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('addAttributesIfMissing', () => {
90
+ it('adds attributes if missing', () => {
91
+ const source = `<div foo />`;
92
+ const root = getRoot(source);
93
+ const element = root.findJSXElements('div').nodes()[0];
94
+ const j = jscodeshift;
95
+ const newAttr = j.jsxAttribute(j.jsxIdentifier('bar'));
96
+ addAttributesIfMissing(j, element.openingElement, [{ attribute: newAttr, name: 'bar' }]);
97
+ expect(hasAttributeOnElement(element.openingElement, 'bar')).toBe(true);
98
+ });
99
+
100
+ it('does not add attribute if already present', () => {
101
+ const source = `<div foo />`;
102
+ const root = getRoot(source);
103
+ const element = root.findJSXElements('div').nodes()[0];
104
+ const j = jscodeshift;
105
+ const existingAttr = element.openingElement.attributes?.find(
106
+ (attr) =>
107
+ attr.type === 'JSXAttribute' &&
108
+ attr.name.type === 'JSXIdentifier' &&
109
+ attr.name.name === 'foo',
110
+ ) as JSXAttribute;
111
+ addAttributesIfMissing(j, element.openingElement, [{ attribute: existingAttr, name: 'foo' }]);
112
+ // attributes length should remain 1
113
+ expect(
114
+ element.openingElement.attributes?.filter((attr) => attr.type === 'JSXAttribute').length,
115
+ ).toBe(1);
116
+ });
117
+
118
+ it('does nothing if openingElement.attributes is not an array', () => {
119
+ const source = `<div />`;
120
+ const root = getRoot(source);
121
+ const element = root.findJSXElements('div').nodes()[0];
122
+ const j = jscodeshift;
123
+ element.openingElement.attributes = null as never;
124
+ const newAttr = j.jsxAttribute(j.jsxIdentifier('bar'));
125
+ expect(() =>
126
+ addAttributesIfMissing(j, element.openingElement, [{ attribute: newAttr, name: 'bar' }]),
127
+ ).not.toThrow();
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,265 @@
1
+ import type { JSCodeshift, JSXAttribute, JSXElement } from 'jscodeshift';
2
+
3
+ import { CodemodReporter, createReporter } from '../jsxReportingUtils';
4
+
5
+ describe('CodemodReporter', () => {
6
+ let issues: string[];
7
+ let reporter: CodemodReporter;
8
+ let mockJ: JSCodeshift;
9
+
10
+ beforeEach(() => {
11
+ issues = [];
12
+
13
+ // Create a proper JSCodeshift mock
14
+ mockJ = Object.assign(
15
+ jest.fn(() => ({
16
+ find: jest.fn(),
17
+ toSource: jest.fn(() => 'mockedSource'),
18
+ })),
19
+ {
20
+ toSource: jest.fn(() => 'mockedSource'),
21
+ types: {},
22
+ match: jest.fn(),
23
+ template: jest.fn(),
24
+ registerMethods: jest.fn(),
25
+ },
26
+ ) as unknown as JSCodeshift;
27
+
28
+ reporter = createReporter(mockJ, issues);
29
+ });
30
+
31
+ // Helper function to create properly typed mock elements
32
+ const createMockElement = (componentName: string, line = 42): JSXElement =>
33
+ ({
34
+ type: 'JSXElement',
35
+ openingElement: {
36
+ type: 'JSXOpeningElement',
37
+ name: { type: 'JSXIdentifier', name: componentName },
38
+ attributes: [],
39
+ selfClosing: false,
40
+ },
41
+ closingElement: null,
42
+ children: [],
43
+ loc: { start: { line, column: 0 }, end: { line, column: 0 } },
44
+ }) as unknown as JSXElement;
45
+
46
+ describe('createReporter', () => {
47
+ it('should create a reporter instance', () => {
48
+ expect(reporter).toBeInstanceOf(CodemodReporter);
49
+ });
50
+ });
51
+
52
+ describe('reportElement', () => {
53
+ it('should report element issues', () => {
54
+ const mockElement = createMockElement('Button', 42);
55
+
56
+ reporter.reportElement(mockElement, 'has some issue');
57
+ expect(issues).toHaveLength(1);
58
+ expect(issues[0]).toBe('Manual review required: <Button> at line 42 has some issue.');
59
+ });
60
+ });
61
+
62
+ describe('reportProp', () => {
63
+ it('should report prop issues', () => {
64
+ const mockElement = createMockElement('Button', 42);
65
+
66
+ reporter.reportProp(mockElement, 'size', 'has invalid value');
67
+ expect(issues).toHaveLength(1);
68
+ expect(issues[0]).toBe(
69
+ 'Manual review required: prop "size" on <Button> at line 42 has invalid value.',
70
+ );
71
+ });
72
+ });
73
+
74
+ describe('reportAttribute', () => {
75
+ it('should report attribute issues with default reason', () => {
76
+ const mockElement = createMockElement('Button', 42);
77
+
78
+ const mockAttr = {
79
+ type: 'JSXAttribute',
80
+ name: { type: 'JSXIdentifier', name: 'size' },
81
+ value: {
82
+ type: 'JSXExpressionContainer',
83
+ expression: { type: 'Identifier', name: 'someVar' },
84
+ },
85
+ loc: { start: { line: 42, column: 0 }, end: { line: 42, column: 0 } },
86
+ } as unknown as JSXAttribute;
87
+
88
+ reporter.reportAttribute(mockAttr, mockElement);
89
+ expect(issues).toHaveLength(1);
90
+ expect(issues[0]).toContain('prop "size" on <Button> at line 42');
91
+ });
92
+
93
+ it('should report attribute issues with custom reason', () => {
94
+ const mockElement = createMockElement('Button', 42);
95
+
96
+ const mockAttr = {
97
+ type: 'JSXAttribute',
98
+ name: { type: 'JSXIdentifier', name: 'size' },
99
+ value: { type: 'StringLiteral', value: 'large' },
100
+ loc: { start: { line: 42, column: 0 }, end: { line: 42, column: 0 } },
101
+ } as unknown as JSXAttribute;
102
+
103
+ reporter.reportAttribute(mockAttr, mockElement, 'custom reason');
104
+ expect(issues).toHaveLength(1);
105
+ expect(issues[0]).toBe(
106
+ 'Manual review required: prop "size" on <Button> at line 42 custom reason.',
107
+ );
108
+ });
109
+ });
110
+
111
+ describe('reportSpreadProps', () => {
112
+ it('should report spread props issue', () => {
113
+ const mockElement = createMockElement('Button', 42);
114
+
115
+ reporter.reportSpreadProps(mockElement);
116
+ expect(issues).toHaveLength(1);
117
+ expect(issues[0]).toBe(
118
+ 'Manual review required: <Button> at line 42 contains spread props that need manual review.',
119
+ );
120
+ });
121
+ });
122
+
123
+ describe('reportPropWithChildren', () => {
124
+ it('should report prop with children issue', () => {
125
+ const mockElement = createMockElement('Button', 42);
126
+
127
+ reporter.reportPropWithChildren(mockElement, 'text');
128
+ expect(issues).toHaveLength(1);
129
+ expect(issues[0]).toBe(
130
+ 'Manual review required: prop "text" on <Button> at line 42 conflicts with children - both "text" prop and children are present.',
131
+ );
132
+ });
133
+ });
134
+
135
+ describe('reportUnsupportedValue', () => {
136
+ it('should report unsupported value issue', () => {
137
+ const mockElement = createMockElement('Button', 42);
138
+
139
+ reporter.reportUnsupportedValue(mockElement, 'size', 'huge');
140
+ expect(issues).toHaveLength(1);
141
+ expect(issues[0]).toBe(
142
+ 'Manual review required: prop "size" on <Button> at line 42 has unsupported value "huge".',
143
+ );
144
+ });
145
+ });
146
+
147
+ describe('reportAmbiguousExpression', () => {
148
+ it('should report ambiguous expression issue', () => {
149
+ const mockElement = createMockElement('Button', 42);
150
+
151
+ reporter.reportAmbiguousExpression(mockElement, 'size');
152
+ expect(issues).toHaveLength(1);
153
+ expect(issues[0]).toBe(
154
+ 'Manual review required: prop "size" on <Button> at line 42 contains a complex expression that needs manual review.',
155
+ );
156
+ });
157
+ });
158
+
159
+ describe('reportAmbiguousChildren', () => {
160
+ it('should report ambiguous children issue', () => {
161
+ const mockElement = createMockElement('Button', 42);
162
+
163
+ reporter.reportAmbiguousChildren(mockElement, 'icon');
164
+ expect(issues).toHaveLength(1);
165
+ expect(issues[0]).toBe(
166
+ 'Manual review required: <Button> at line 42 contains ambiguous icon that needs manual review.',
167
+ );
168
+ });
169
+ });
170
+
171
+ describe('reportDeprecatedProp', () => {
172
+ it('should report deprecated prop without alternative', () => {
173
+ const mockElement = createMockElement('Button', 42);
174
+
175
+ reporter.reportDeprecatedProp(mockElement, 'flat');
176
+ expect(issues).toHaveLength(1);
177
+ expect(issues[0]).toBe(
178
+ 'Manual review required: prop "flat" on <Button> at line 42 is deprecated.',
179
+ );
180
+ });
181
+
182
+ it('should report deprecated prop with alternative', () => {
183
+ const mockElement = createMockElement('Button', 42);
184
+
185
+ reporter.reportDeprecatedProp(mockElement, 'flat', 'variant="text"');
186
+ expect(issues).toHaveLength(1);
187
+ expect(issues[0]).toBe(
188
+ 'Manual review required: prop "flat" on <Button> at line 42 is deprecated Use variant="text" instead.',
189
+ );
190
+ });
191
+ });
192
+
193
+ describe('reportMissingRequiredProp', () => {
194
+ it('should report missing required prop', () => {
195
+ const mockElement = createMockElement('Button', 42);
196
+
197
+ reporter.reportMissingRequiredProp(mockElement, 'children');
198
+ expect(issues).toHaveLength(1);
199
+ expect(issues[0]).toBe(
200
+ 'Manual review required: prop "children" on <Button> at line 42 is required but missing.',
201
+ );
202
+ });
203
+ });
204
+
205
+ describe('reportConflictingProps', () => {
206
+ it('should report conflicting props', () => {
207
+ const mockElement = createMockElement('Button', 42);
208
+
209
+ reporter.reportConflictingProps(mockElement, ['size', 'fullWidth']);
210
+ expect(issues).toHaveLength(1);
211
+ expect(issues[0]).toBe(
212
+ 'Manual review required: <Button> at line 42 has conflicting props: "size", "fullWidth" cannot be used together.',
213
+ );
214
+ });
215
+ });
216
+
217
+ describe('reportAttributeIssues', () => {
218
+ it('should not report anything with no attributes', () => {
219
+ const mockElement = createMockElement('Button', 42);
220
+
221
+ reporter.reportAttributeIssues(mockElement);
222
+ expect(issues).toHaveLength(0);
223
+ });
224
+
225
+ it('should report spread props', () => {
226
+ const mockElement = {
227
+ ...createMockElement('Button', 42),
228
+ openingElement: {
229
+ ...createMockElement('Button', 42).openingElement,
230
+ attributes: [
231
+ { type: 'JSXSpreadAttribute', argument: { type: 'Identifier', name: 'props' } },
232
+ ],
233
+ },
234
+ } as unknown as JSXElement;
235
+
236
+ reporter.reportAttributeIssues(mockElement);
237
+ expect(issues).toHaveLength(1);
238
+ expect(issues[0]).toContain('spread props');
239
+ });
240
+
241
+ it('should report expression container attributes', () => {
242
+ const mockElement = {
243
+ ...createMockElement('Button', 42),
244
+ openingElement: {
245
+ ...createMockElement('Button', 42).openingElement,
246
+ attributes: [
247
+ {
248
+ type: 'JSXAttribute',
249
+ name: { type: 'JSXIdentifier', name: 'size' },
250
+ value: {
251
+ type: 'JSXExpressionContainer',
252
+ expression: { type: 'Identifier', name: 'someVar' },
253
+ },
254
+ loc: { start: { line: 42, column: 0 }, end: { line: 42, column: 0 } },
255
+ },
256
+ ],
257
+ },
258
+ } as unknown as JSXElement;
259
+
260
+ reporter.reportAttributeIssues(mockElement);
261
+ expect(issues).toHaveLength(1);
262
+ expect(issues[0]).toContain('prop "size" on <Button> at line 42');
263
+ });
264
+ });
265
+ });