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();
+ },
+};