diff --git a/packages/mcp-server/src/mcp/autofixers/add-autofixers-issues.test.ts b/packages/mcp-server/src/mcp/autofixers/add-autofixers-issues.test.ts index cceaf19..1703f47 100644 --- a/packages/mcp-server/src/mcp/autofixers/add-autofixers-issues.test.ts +++ b/packages/mcp-server/src/mcp/autofixers/add-autofixers-issues.test.ts @@ -374,4 +374,177 @@ describe('add_autofixers_issues', () => { ); }); }); + + describe('suggest_attachments', () => { + describe('bind:this', () => { + it('should add suggestions when using bind:this on an element', () => { + const content = run_autofixers_on_code(` + + + `); + + expect(content.suggestions.length).toBeGreaterThanOrEqual(1); + expect(content.suggestions).toContain( + 'The usage of `bind:this` can often be replaced with an easier to read `action` or even better an `attachment`. Consider using the latter if possible.', + ); + }); + + it('should not add suggestions when using bind:this on a component', () => { + const content = run_autofixers_on_code(` + + + `); + + expect(content.suggestions).not.toContain( + 'The usage of `bind:this` can often be replaced with an easier to read `action` or even better an `attachment`. Consider using the latter if possible.', + ); + }); + + it('should not add suggestions when using bind:this on a component nested in an element', () => { + const content = run_autofixers_on_code(` + + +
+ +
`); + + expect(content.suggestions).not.toContain( + 'The usage of `bind:this` can often be replaced with an easier to read `action` or even better an `attachment`. Consider using the latter if possible.', + ); + }); + + it('should add suggestions but not suggest attachments when using bind:this on an element and the desired svelte version is 4', () => { + const content = run_autofixers_on_code( + ` + + +
`, + 4, + ); + + expect(content.suggestions.length).toBeGreaterThanOrEqual(1); + expect(content.suggestions).toContain( + 'The usage of `bind:this` can often be replaced with an easier to read `action`. Consider using the latter if possible.', + ); + }); + }); + + describe('use:', () => { + it('should add suggestions when using use: on an element and the action is declared as a function', () => { + const content = run_autofixers_on_code( + ` + + `, + ); + + expect(content.suggestions.length).toBeGreaterThanOrEqual(1); + expect(content.suggestions).toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + + it('should add suggestions when using use: on an element and the action is declared as a variable', () => { + const content = run_autofixers_on_code( + ` + + `, + ); + + expect(content.suggestions.length).toBeGreaterThanOrEqual(1); + expect(content.suggestions).toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + + it('should add suggestions when using use: on an element and the action is declared as an object', () => { + const content = run_autofixers_on_code( + ` + + `, + ); + + expect(content.suggestions.length).toBeGreaterThanOrEqual(1); + expect(content.suggestions).toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + + it('should not add suggestions when using use: on an element and the desired svelte version is 4', () => { + const content = run_autofixers_on_code( + ` + + `, + 4, + ); + + expect(content.suggestions).not.toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + + it('should not add suggestions when using use: on an element and the action comes from an import', () => { + const content = run_autofixers_on_code( + ` + + `, + ); + + expect(content.suggestions).not.toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + + it('should not add suggestions when using use: on an element and the action comes from the props', () => { + const content = run_autofixers_on_code( + ` + + `, + ); + + expect(content.suggestions).not.toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + + it('should not add suggestions when using use: on an element and the action comes from a global variable', () => { + const content = run_autofixers_on_code(``); + + expect(content.suggestions).not.toContain( + 'Consider using an `attachment` instead of an `action` for "my_action".', + ); + }); + }); + }); }); diff --git a/packages/mcp-server/src/mcp/autofixers/visitors/index.ts b/packages/mcp-server/src/mcp/autofixers/visitors/index.ts index b29f303..3b0c598 100644 --- a/packages/mcp-server/src/mcp/autofixers/visitors/index.ts +++ b/packages/mcp-server/src/mcp/autofixers/visitors/index.ts @@ -16,3 +16,4 @@ export * from './set-or-update-state.js'; export * from './imported-runes.js'; export * from './derived-with-function.js'; export * from './use-runes-instead-of-store.js'; +export * from './suggest-attachments.js'; diff --git a/packages/mcp-server/src/mcp/autofixers/visitors/suggest-attachments.ts b/packages/mcp-server/src/mcp/autofixers/visitors/suggest-attachments.ts new file mode 100644 index 0000000..778b826 --- /dev/null +++ b/packages/mcp-server/src/mcp/autofixers/visitors/suggest-attachments.ts @@ -0,0 +1,47 @@ +import type { Identifier } from 'estree'; +import type { Autofixer } from './index.js'; +import { left_most_id } from '../ast/utils.js'; + +export const suggest_attachments: Autofixer = { + SvelteDirective(node, { state, next, path }) { + if (node.kind === 'Binding' && node.key.name.name === 'this') { + const parent_element = path.findLast((p) => p.type === 'SvelteElement'); + if (parent_element?.kind === 'html' && parent_element.startTag.attributes.includes(node)) { + let better_an_attachment = ` or even better an \`attachment\``; + if (state.desired_svelte_version === 4) { + better_an_attachment = ``; + } + state.output.suggestions.push( + `The usage of \`bind:this\` can often be replaced with an easier to read \`action\`${better_an_attachment}. Consider using the latter if possible.`, + ); + } + } else if (node.kind === 'Action' && state.desired_svelte_version === 5) { + let id: Identifier | null = null; + if (node.key.name.type === 'Identifier') { + id = node.key.name; + } else if (node.key.name.type === 'MemberExpression') { + id = left_most_id(node.key.name); + } + if (id) { + const reference = state.parsed.find_reference_by_id(id); + const definition = reference?.resolved?.defs[0]; + console.log(definition); + if ( + definition && + (definition.type === 'Variable' || + !(definition.type === 'ImportBinding' || definition.type === 'Parameter')) && + !( + definition.type === 'Variable' && + definition.node.init?.type === 'CallExpression' && + state.parsed.is_rune(definition.node.init, ['$props']) + ) + ) { + state.output.suggestions.push( + `Consider using an \`attachment\` instead of an \`action\` for "${id.name}".`, + ); + } + } + } + next(); + }, +};