sveltejs / language-tools

The Svelte Language Server, and official extensions which use it
MIT License
1.22k stars 195 forks source link

svelte-check shows `Cannot find name '<snippet>'` error when snippet is defined outside component it's being used for #2425

Closed sheijne closed 2 months ago

sheijne commented 3 months ago

Describe the bug

When running svelte-check on a file which defines a snippet "outside" an component/element, svelte-check will show the following error:

Error: Cannot find name 'snippet'. (ts)
    <Button children={hasChildren ? renderChildren : undefined}></Button>

Reproduction

Running svelte-check against the following code will trigger the error:

<script lang="ts">
  import SomeComponent from './SomeComponent.svelte';
  import Button from './Button.svelte';

  let hasChildren = $state(false);
</script>

<SomeComponent>
  {#snippet renderChildren()}Hello world!{/snippet}

  <!-- All these trigger the error -->
  <Button children={hasChildren ? renderChildren : undefined}></Button>
  <Button>{@render renderChildren()}</Button>
  <Button>{#snippet children()}{@render renderChildren()}{/snippet}</Button>
</SomeComponent>

Where SomeComponent and Button are arbitrary.

<script>
  let { children } = $props();
</script>

<button>{@render children?.()}</button>

Expected behaviour

No error to be shown, and type-checking to pass. The same snippet of code works at runtime, also when there is a wrapper component.

System Info

System: OS: macOS 14.5 CPU: (10) arm64 Apple M1 Pro Memory: 1.20 GB / 32.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.11.1 - ~/.nvm/versions/node/v20.11.1/bin/node npm: 10.2.4 - ~/.nvm/versions/node/v20.11.1/bin/npm pnpm: 9.4.0 - ~/.nvm/versions/node/v20.11.1/bin/pnpm bun: 1.1.13 - ~/.bun/bin/bun Browsers: Chrome: 126.0.6478.127 Edge: 126.0.2592.68 Safari: 17.5 Packages: svelte: 5.0.0-next.166 svelte-check: 3.8.4

Which package is the issue about?

svelte-check

Additional Information, eg. Screenshots

No response

sheijne commented 3 months ago

I feel a bit silly now, but I found the cause here. I had the following structure in my component:

<SomeComponent>
  {#snippet renderChildren()}Hello world!{/snippet}
  <Button children={hasChildren ? renderChildren : undefined}></Button>
</SomeOtherComponent>

The error seems to be caused due to the fact that the renderChildren snippet is being passed to SomeComponent as a prop, which does not have this prop defined, and triggers another error. My assumption here is that TS or svelte-check consequently fails to recognise the renderChildren snippet once it reaches renderChildren inside the Button component.

Will change the reproduction, and if this is intended behavior (despite it working at runtime), the issue can be closed.

Edit: even with a prop called renderChildren defined on SomeComponent the error is still triggered. It only seems to pass when the snippet is defined at the top-level.

brunnerh commented 3 months ago

This looks like a situation where children should be defined explicitly to avoid passing the snippet as prop at the same time (regardless of the language tool error).

  <SomeComponent>
+   {#snippet children()}
      {#snippet renderChildren()}Hello world!{/snippet}
      <Button children={hasChildren ? renderChildren : undefined}></Button>
+   {/snippet}
  </SomeComponent>

Maybe when a snippet is referenced somewhere and passed as a prop at the same time, a warning could be shown, something like:

Snippet is being passed as property ... to component ..., define a children snippet ...

sheijne commented 3 months ago

Interesting, that could be an option. It also seems to eliminate the type error actually, which somewhat surprised me.

jasonlyu123 commented 3 months ago

The problem is that this is treated as implicit props so no virtual variable is generated for the snippet. Instead, it's directly passed to SomeCommponent as props. So yes. making it not an implicit prop is a workaround and probably a better solution for your case.

sheijne commented 3 months ago

The issue I'm trying to raise is more regarding the code not being parsed correctly by svelte-check.

According to the preview docs:

and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings)

Which is true at runtime. However svelte-check seems to think the snippet is not in the same lexical scope.

jasonlyu123 commented 3 months ago

Behind the scenes, a virtual ts is generated so TypeScript can understand the svelte file. What I meant is that because it is an implicit prop. The generated snippet is directly passed to the component as a prop and no variable representation for the snippet was generated. That is why typescript complain that the variable doesn't exist.

new Component({ props: { renderChildren: () => {} } })

instead of

var renderChildren = () => {}
new Component({ props: { renderChildren } })
jasonlyu123 commented 2 months ago

Note for self, one advantage of inlining the snippet as Component props is that the type is inferred. If we always transform this as a variable the parameter type won't be inferred. We might need to track usage and only add the variable if needed but that would be tricky to do with static analysis without type check.