Compare commits

...

5 Commits

Author SHA1 Message Date
paoloricciuti
199d57a8e3 feat: wip github webhook 2025-09-26 14:59:47 +02:00
paoloricciuti
47fa0a4382 fix: add $derived and $derived.by to wrong-property-access-state 2025-09-26 12:39:40 +02:00
paoloricciuti
4c6232a44f fix: add $derived and $derived.by to the reassignment list 2025-09-26 12:33:33 +02:00
paoloricciuti
8edbf2f36b fix: disable todos tools 2025-09-26 12:15:11 +02:00
paoloricciuti
6e54719f88 chore: update deps to fix tmcp issue 2025-09-26 10:01:45 +02:00
12 changed files with 540 additions and 598 deletions

View File

@@ -1,3 +1,4 @@
DATABASE_URL=file:test.db
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
VOYAGE_API_KEY=your_actual_api_key_here
VOYAGE_API_KEY=your_actual_api_key_here
GITHUB_WEBHOOK_SECRET=some_secret

View File

@@ -64,6 +64,8 @@
"dependencies": {
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "^0.6.2"
"@tmcp/transport-http": "^0.6.3",
"tmcp": "^1.14.0",
"valibot": "^1.1.0"
}
}

View File

@@ -0,0 +1,32 @@
import * as v from 'valibot';
// not the full schema but it contains the information we need
export const github_webhook_schema = v.object({
action: v.union([v.literal('closed')]),
pull_request: v.object({
patch_url: v.string(),
merged: v.boolean(),
user: v.object({
login: v.string(),
}),
}),
});
export const github_content_schema = v.object({
name: v.string(),
path: v.string(),
sha: v.string(),
size: v.number(),
url: v.string(),
html_url: v.string(),
git_url: v.string(),
download_url: v.nullable(v.string()),
type: v.literal('file'),
content: v.string(),
encoding: v.literal('base64'),
_links: v.object({
self: v.string(),
git: v.string(),
html: v.string(),
}),
});

View File

@@ -0,0 +1,54 @@
import { github_content_schema, github_webhook_schema } from '$lib/schemas/index.js';
import * as v from 'valibot';
export async function POST({ request, fetch }) {
const body = await request.json();
// TODO add secret validation
const validated_pull_request = v.safeParse(github_webhook_schema, body);
if (!validated_pull_request.success) {
return new Response('Invalid payload', { status: 400 });
}
const { pull_request } = validated_pull_request.output;
if (!pull_request.merged) {
return new Response(null, { status: 204 });
}
const patch = await fetch(pull_request.patch_url);
if (!patch.ok) {
return new Response('Failed to fetch patch', { status: 500 });
}
const patch_text = await patch.text();
const files = [
...patch_text.matchAll(
/^diff --git\sa\/(?<file>.+?)\sb\/\1\n(?:(?<action>deleted|new)\sfile mode)?/gm,
),
].map((res) => ({ file: res.groups!.file, action: res.groups?.action ?? 'modified' }));
for (const file of files) {
if (file.action === 'deleted') {
// delete path from db
continue;
}
const new_file_content = await fetch(
`https://api.github.com/repos/sveltejs/svelte.dev/contents/${file.file}`,
);
if (!new_file_content.ok) {
// push file in queue and try again later?
continue;
}
const new_file_json = await new_file_content.json();
const validated_content = v.safeParse(github_content_schema, new_file_json);
if (!validated_content.success) {
// push file in queue and try again later?
continue;
}
const content = Buffer.from(validated_content.output.content, 'base64').toString('utf-8');
// save content and distilled content in the db
console.log({ content, file: file.file });
}
return new Response(null, { status: 204 });
}

View File

@@ -10,279 +10,292 @@ function run_autofixers_on_code(code: string, desired_svelte_version = 5) {
return content;
}
function with_possible_inits(title: string, fn: (args: { init: string }) => void) {
describe.each([
{ init: '$state' },
{ init: '$state.raw' },
{ init: '$derived' },
{ init: '$derived.by' },
])(title, fn);
}
describe('add_autofixers_issues', () => {
describe('assign_in_effect', () => {
it(`should add suggestions when assigning to a stateful variable inside an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
$effect(() => {
count = 43;
});
</script>`);
with_possible_inits('($init)', ({ init }) => {
it(`should add suggestions when assigning to a stateful variable inside an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(0);
$effect(() => {
count = 43;
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for each variable assigned within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
const count2 = $state(0);
$effect(() => {
count = 43;
count2 = 44;
});
</script>`);
it(`should add a suggestion for each variable assigned within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
const count2 = $state(0);
$effect(() => {
count = 43;
count2 = 44;
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
expect(content.suggestions).toContain(
'The stateful variable "count2" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should not add a suggestion for variables that are not assigned within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
</script>
<button onclick={() => count = 43}>Increment</button>
`);
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it("should not add a suggestions for variables that are assigned within an effect but aren't stateful", () => {
const content = run_autofixers_on_code(`
<script>
const count = 0;
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
expect(content.suggestions).toContain(
'The stateful variable "count2" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should not add a suggestion for variables that are not assigned within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = ${init}(0);
</script>
$effect(() => {
count = 43;
});
</script>`);
<button onclick={() => count = 43}>Increment</button>
`);
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for variables that are assigned within an effect with an update`, () => {
const content = run_autofixers_on_code(`
<script>
let count = $state(0);
$effect(() => {
count++;
});
</script>
`);
it("should not add a suggestions for variables that are assigned within an effect but aren't stateful", () => {
const content = run_autofixers_on_code(`
<script>
const count = 0;
$effect(() => {
count = 43;
});
</script>`);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for variables that are mutated within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
let count = $state({ value: 0 });
$effect(() => {
count.value = 42;
});
</script>
`);
it(`should add a suggestion for variables that are assigned within an effect with an update`, () => {
const content = run_autofixers_on_code(`
<script>
let count = ${init}(0);
$effect(() => {
count++;
});
</script>
`);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for variables that are mutated within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
let count = ${init}({ value: 0 });
$effect(() => {
count.value = 42;
});
</script>
`);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
});
});
describe.each([{ method: 'set' }, { method: 'update' }])(
'wrong_property_access_state ($method)',
({ method }) => {
it(`should add suggestions when using .${method}() on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
with_possible_inits('($init)', ({ init }) => {
describe.each([{ method: 'set' }, { method: 'update' }])(
'wrong_property_access_state ($method)',
({ method }) => {
it(`should add suggestions when using .${method}() on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
const count = ${init}(0);
function update_count() {
count.${method}(43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when using .${method}() on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state([0]);
const count = ${init}([0]);
function update_count() {
count.${method}([1]);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable ($state({}))`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable (${init}({}))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state({ value: 0 });
const count = ${init}({ value: 0 });
function update_count() {
count.${method}({ value: 43 });
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable ($state(new Class()))`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable (${init}(new Class()))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(new Class());
const count = ${init}(new Class());
function update_count() {
count.${method}(new Class());
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable ($state(variable_name))`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable (${init}(variable_name))`, () => {
const content = run_autofixers_on_code(`
<script>
const { init } = $props();
const count = $state(init);
const count = ${init}(init);
function update_count() {
count.${method}(43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should not add suggestions when using .${method} on a stateful variable if it's not a method call`, () => {
const content = run_autofixers_on_code(`
it(`should not add suggestions when using .${method} on a stateful variable if it's not a method call`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state({});
const count = ${init}({});
function update_count() {
console.log(count.${method});
}
</script>`);
expect(content.suggestions).not.toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
},
);
expect(content.suggestions).not.toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
},
);
describe.each([{ property: '$' }])(
'wrong_property_access_state property ($property)',
async ({ property }) => {
it(`should add suggestions when reading .${property} on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
describe.each([{ property: '$' }])(
'wrong_property_access_state property ($property)',
async ({ property }) => {
it(`should add suggestions when reading .${property} on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
const count = ${init}(0);
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when reading .${property} on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state([1]);
const count = ${init}([1]);
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable ($state({}))`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable (${init}({}))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state({ value: 0 });
const count = ${init}({ value: 0 });
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable ($state(new Class()))`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable (${init}(new Class()))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(new Class());
const count = ${init}(new Class());
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable ($state(variable_name))`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when reading .${property} on a stateful variable with conditional if it's not sure if the property could actually be present on the variable (${init}(variable_name))`, () => {
const content = run_autofixers_on_code(`
<script>
const { init } = $props();
const count = $state(init);
const count = ${init}(init);
function read_count() {
count.${property};
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
},
);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to read the stateful variable "count" using "${property}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them. However I can't verify if "count" is a state variable of an object or a class with a "${property}" property on it. Please verify that before updating the code to use a normal access`,
);
});
},
);
});
describe('imported_runes', () => {
describe.each([{ source: 'svelte' }, { source: 'svelte/runes' }])(

View File

@@ -39,7 +39,7 @@ function visitor(
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
state.parsed.is_rune(init, ['$state', '$state.raw', '$derived', '$derived.by'])
) {
state.output.suggestions.push(
`The stateful variable "${id.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,

View File

@@ -22,7 +22,7 @@ export const wrong_property_access_state: Autofixer = {
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
state.parsed.is_rune(init, ['$state', '$state.raw', '$derived', '$derived.by'])
) {
let suggestion = is_property
? `You are trying to read the stateful variable "${id.name}" using "${node.property.name}". stateful variables should be read just by accessing them like normal variable, do not use properties to read them.`

View File

@@ -4,6 +4,7 @@ export function list_sections(server: SvelteMcp) {
server.resource(
{
name: 'list-sections',
enabled: () => false,
description:
'The list of all the available Svelte 5 and SvelteKit documentation sections in a structured format.',
uri: 'svelte://list-sections',

View File

@@ -5,6 +5,7 @@ export function get_documentation(server: SvelteMcp) {
server.tool(
{
name: 'get-documentation',
enabled: () => false,
description:
'Retrieves full documentation content for Svelte 5 or SvelteKit sections. Supports flexible search by title (e.g., "$state", "routing") or file path (e.g., "docs/svelte/state.md"). Can accept a single section name or an array of sections. Before running this, make sure to analyze the users query, as well as the output from list_sections (which should be called first). Then ask for ALL relevant sections the user might require. For example, if the user asks to build anything interactive, you will need to fetch all relevant runes, and so on.',
schema: v.object({

View File

@@ -4,6 +4,7 @@ export function list_sections(server: SvelteMcp) {
server.tool(
{
name: 'list-sections',
enabled: () => false,
description:
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Returns sections as a list of "* title: [section_title], path: [file_path]" - you can use either the title or path when querying a specific section via the get_documentation tool. Always run list_sections first for any query related to Svelte development to discover available content.',
},

View File

@@ -29,7 +29,7 @@
},
"devDependencies": {
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-stdio": "^0.3.0",
"@tmcp/transport-stdio": "^0.3.1",
"@types/node": "^22.15.17",
"publint": "^0.3.13",
"tsdown": "^0.11.9",

661
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff