Skip to content

Commit 17b47eb

Browse files
authored
refactor(eslint-plugin-internal): use unwrapExpression in getObjectProperty [AR-55075] (#362)
1 parent 7868af1 commit 17b47eb

7 files changed

Lines changed: 227 additions & 16 deletions

File tree

packages/eslint-plugin-internal/src/rules/__tests__/no-autodocs-tag.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,93 @@ ruleTester.run('no-autodocs-tag', noAutodocsTag, {
168168
],
169169
},
170170

171+
{
172+
name: 'autodocs with satisfies on tags value',
173+
code: `
174+
const meta = {
175+
title: 'Design System/Button',
176+
tags: ['autodocs'] satisfies string[],
177+
};
178+
179+
export default meta;
180+
`,
181+
output: `
182+
const meta = {
183+
title: 'Design System/Button',
184+
185+
};
186+
187+
export default meta;
188+
`,
189+
errors: [
190+
{
191+
messageId: 'noAutodocsTag',
192+
line: 4,
193+
endLine: 4,
194+
column: 13,
195+
endColumn: 23,
196+
},
197+
],
198+
},
199+
200+
{
201+
name: 'autodocs in the middle with satisfies on tags value',
202+
code: `
203+
const meta = {
204+
title: 'Design System/Button',
205+
tags: ['!dev', 'autodocs', 'deprecated'] satisfies string[],
206+
};
207+
208+
export default meta;
209+
`,
210+
output: `
211+
const meta = {
212+
title: 'Design System/Button',
213+
tags: ['!dev', 'deprecated'] satisfies string[],
214+
};
215+
216+
export default meta;
217+
`,
218+
errors: [
219+
{
220+
messageId: 'noAutodocsTag',
221+
line: 4,
222+
endLine: 4,
223+
column: 21,
224+
endColumn: 31,
225+
},
226+
],
227+
},
228+
229+
{
230+
name: 'autodocs with as assertion on tags value',
231+
code: `
232+
const meta = {
233+
title: 'Design System/Button',
234+
tags: ['autodocs'] as string[],
235+
};
236+
237+
export default meta;
238+
`,
239+
output: `
240+
const meta = {
241+
title: 'Design System/Button',
242+
243+
};
244+
245+
export default meta;
246+
`,
247+
errors: [
248+
{
249+
messageId: 'noAutodocsTag',
250+
line: 4,
251+
endLine: 4,
252+
column: 13,
253+
endColumn: 23,
254+
},
255+
],
256+
},
257+
171258
{
172259
name: 'inline export with autodocs',
173260
code: `

packages/eslint-plugin-internal/src/rules/__tests__/no-empty-story.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ ruleTester.run('no-empty-story', noEmptyStory, {
3939
`,
4040
},
4141

42+
{
43+
name: 'non-empty args with satisfies',
44+
code: `export const Primary = { args: { label: 'Hello' } satisfies Args };`,
45+
},
46+
47+
{
48+
name: 'non-empty args with as assertion',
49+
code: `export const Primary = { args: { label: 'Hello' } as Args };`,
50+
},
51+
4252
{
4353
name: 'default export with empty object',
4454
code: `export default {};`,
@@ -121,6 +131,34 @@ ruleTester.run('no-empty-story', noEmptyStory, {
121131
],
122132
},
123133

134+
{
135+
name: 'empty args with satisfies',
136+
code: `export const Primary = { args: {} satisfies Args };`,
137+
errors: [
138+
{
139+
messageId: 'noEmptyStory',
140+
line: 1,
141+
endLine: 1,
142+
column: 24,
143+
endColumn: 51,
144+
},
145+
],
146+
},
147+
148+
{
149+
name: 'empty args with as assertion',
150+
code: `export const Primary = { args: {} as Args };`,
151+
errors: [
152+
{
153+
messageId: 'noEmptyStory',
154+
line: 1,
155+
endLine: 1,
156+
column: 24,
157+
endColumn: 44,
158+
},
159+
],
160+
},
161+
124162
{
125163
name: 'variable reference to empty object',
126164
code: `

packages/eslint-plugin-internal/src/rules/__tests__/no-useless-story-annotations.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ ruleTester.run('no-useless-story-annotations', noUselessStoryAnnotations, {
8787
code: `export const Default = { storyName: 'Custom Name' };`,
8888
},
8989

90+
// --- satisfies / as on property values ---
91+
{
92+
name: 'non-empty args with satisfies on value',
93+
code: `export const Primary = { args: { label: 'Hello' } satisfies Args };`,
94+
},
95+
{
96+
name: 'non-empty args with as assertion on value',
97+
code: `export const Primary = { args: { label: 'Hello' } as Args };`,
98+
},
99+
{
100+
name: 'non-empty tags with as assertion on value',
101+
code: `export const Primary = { tags: ['deprecated'] as string[] };`,
102+
},
103+
{
104+
name: 'non-empty parameters with satisfies on value',
105+
code: `export const Primary = { parameters: { layout: 'centered' } satisfies Params };`,
106+
},
107+
{
108+
name: 'non-empty play with satisfies on value',
109+
code: `export const Primary = { play: (async () => { await click(); }) satisfies PlayFn };`,
110+
},
111+
90112
// --- misc ---
91113
{
92114
name: 'story with no annotations',
@@ -489,7 +511,7 @@ ruleTester.run('no-useless-story-annotations', noUselessStoryAnnotations, {
489511
],
490512
},
491513

492-
// --- satisfies / as ---
514+
// --- satisfies / as on story ---
493515
{
494516
name: 'empty args with satisfies expression',
495517
code: `export const Primary = { args: {} } satisfies Story;`,
@@ -533,6 +555,50 @@ ruleTester.run('no-useless-story-annotations', noUselessStoryAnnotations, {
533555
],
534556
},
535557

558+
// --- satisfies / as on property values ---
559+
{
560+
name: 'empty args value with satisfies',
561+
code: `export const Primary = { args: {} satisfies Args, render: () => {} };`,
562+
output: `export const Primary = { render: () => {} };`,
563+
errors: [
564+
{
565+
messageId: 'emptyArgs',
566+
line: 1,
567+
endLine: 1,
568+
column: 26,
569+
endColumn: 30,
570+
},
571+
],
572+
},
573+
{
574+
name: 'empty tags value with as assertion',
575+
code: `export const Primary = { tags: [] as string[] };`,
576+
output: `export const Primary = { };`,
577+
errors: [
578+
{
579+
messageId: 'emptyTags',
580+
line: 1,
581+
endLine: 1,
582+
column: 26,
583+
endColumn: 30,
584+
},
585+
],
586+
},
587+
{
588+
name: 'empty play value with satisfies',
589+
code: `export const Primary = { play: (async () => {}) satisfies PlayFn };`,
590+
output: `export const Primary = { };`,
591+
errors: [
592+
{
593+
messageId: 'emptyPlay',
594+
line: 1,
595+
endLine: 1,
596+
column: 26,
597+
endColumn: 30,
598+
},
599+
],
600+
},
601+
536602
// --- multiple violations ---
537603
{
538604
name: 'multiple useless annotations on one story',

packages/eslint-plugin-internal/src/rules/consistent-deprecated-stories.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const consistentDeprecatedStories = createRule<[], MessageId>({
102102
data: { component: componentDisplayName },
103103
fix: (fixer) => {
104104
const hasOnlyOneTag = tagsArray?.elements.length === 1;
105-
const nodeToRemove = hasOnlyOneTag ? tagsProp : deprecatedTag;
105+
const nodeToRemove = hasOnlyOneTag ? tagsProp?.node : deprecatedTag;
106106

107107
if (!nodeToRemove) {
108108
return null;
@@ -123,7 +123,7 @@ export const consistentDeprecatedStories = createRule<[], MessageId>({
123123
messageId: 'missingDeprecatedSuffix',
124124
data: { component: componentDisplayName },
125125
fix: (fixer) => {
126-
return fixer.insertTextAfter(componentProp, `,\n\ttitle: '${componentName} ${SUFFIX}'`);
126+
return fixer.insertTextAfter(componentProp.node, `,\n\ttitle: '${componentName} ${SUFFIX}'`);
127127
},
128128
});
129129

@@ -147,7 +147,7 @@ export const consistentDeprecatedStories = createRule<[], MessageId>({
147147
// Report when a deprecated component is missing a `deprecated` tag.
148148
if (!hasDeprecatedTag) {
149149
context.report({
150-
node: tagsProp ?? metaNode,
150+
node: tagsProp?.node ?? metaNode,
151151
messageId: 'missingDeprecatedTag',
152152
data: { component: componentDisplayName },
153153
fix: (fixer) => {

packages/eslint-plugin-internal/src/rules/no-autodocs-tag.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const noAutodocsTag = createRule<[], MessageId>({
5656
const elements = tagsProp.value.elements;
5757

5858
const shouldRemoveTagsProp = elements.length === 1;
59-
const nodeToRemove = shouldRemoveTagsProp ? tagsProp : autodocsTag;
59+
const nodeToRemove = shouldRemoveTagsProp ? tagsProp.node : autodocsTag;
6060

6161
return removeWithTrailingComma(sourceCode, fixer, nodeToRemove);
6262
},

packages/eslint-plugin-internal/src/rules/no-useless-story-annotations.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const noUselessStoryAnnotations = createRule<[], MessageId>({
5353
});
5454

5555
if (prop && prop.value.properties.length === 0) {
56-
report(prop, messageId);
56+
report(prop.node, messageId);
5757
}
5858
}
5959

@@ -65,7 +65,7 @@ export const noUselessStoryAnnotations = createRule<[], MessageId>({
6565
});
6666

6767
if (prop && prop.value.elements.length === 0) {
68-
report(prop, messageId);
68+
report(prop.node, messageId);
6969
}
7070
}
7171

@@ -84,7 +84,7 @@ export const noUselessStoryAnnotations = createRule<[], MessageId>({
8484
const { body } = prop.value;
8585

8686
if (body.type === AST_NODE_TYPES.BlockStatement && body.body.length === 0) {
87-
report(prop, 'emptyPlay');
87+
report(prop.node, 'emptyPlay');
8888
}
8989
}
9090

@@ -106,7 +106,7 @@ export const noUselessStoryAnnotations = createRule<[], MessageId>({
106106
const userDefinedName = getStringIfConstant(prop.value, context.sourceCode.getScope(prop.value));
107107

108108
if (userDefinedName === generatedName) {
109-
report(prop, 'redundantName', { name: generatedName });
109+
report(prop.node, 'redundantName', { name: generatedName });
110110
}
111111
}
112112

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
2-
3-
type PropertyValuePredicate<T extends PropertyValue> = (value: PropertyValue) => value is T;
2+
import { unwrapExpression } from './unwrap-expression';
43

54
type PropertyValue = TSESTree.Property['value'];
65

76
type Args<T extends PropertyValue> = {
87
obj: TSESTree.ObjectExpression;
98
name: string;
10-
predicate?: PropertyValuePredicate<T>;
9+
predicate?: (value: TSESTree.Node) => value is T;
10+
};
11+
12+
export type ObjectPropertyResult<T extends PropertyValue> = {
13+
node: TSESTree.Property;
14+
value: T;
1115
};
1216

17+
/**
18+
* Get an object property by name and optional value predicate.
19+
*
20+
* Returns the property node and the unwrapped value (see {@link unwrapExpression}).
21+
*/
1322
export function getObjectProperty<T extends PropertyValue = PropertyValue>(
1423
args: Args<T>,
15-
): (TSESTree.Property & { value: T }) | undefined {
24+
): ObjectPropertyResult<T> | null {
1625
const { obj, name, predicate = () => true } = args;
1726

18-
return obj.properties.find((property) => {
27+
const property = obj.properties.find((property): property is TSESTree.Property => {
1928
return (
2029
property.type === AST_NODE_TYPES.Property &&
2130
property.key.type === AST_NODE_TYPES.Identifier &&
2231
property.key.name === name &&
23-
predicate(property.value)
32+
predicate(unwrapExpression(property.value))
2433
);
25-
}) as never;
34+
});
35+
36+
if (!property) {
37+
return null;
38+
}
39+
40+
return {
41+
node: property,
42+
43+
// At this point we know the unwrapped value is of type T because of the predicate.
44+
value: unwrapExpression(property.value) as T,
45+
};
2646
}

0 commit comments

Comments
 (0)