Skip to content

fix: handle ts expressions when dealing with runes #9681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/twelve-onions-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: handle ts expressions when dealing with runes
10 changes: 6 additions & 4 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
extract_paths,
is_event_attribute,
is_text_attribute,
object
object,
unwrap_ts_expression
} from '../../utils/ast.js';
import * as b from '../../utils/builders.js';
import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
Expand Down Expand Up @@ -660,10 +661,11 @@ const runes_scope_js_tweaker = {
/** @type {import('./types').Visitors} */
const runes_scope_tweaker = {
VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return;
if (get_rune(node.init, state.scope) === null) return;
const init = unwrap_ts_expression(node.init);
if (!init || init.type !== 'CallExpression') return;
if (get_rune(init, state.scope) === null) return;

const callee = node.init.callee;
const callee = init.callee;
if (callee.type !== 'Identifier') return;

const name = callee.name;
Expand Down
11 changes: 8 additions & 3 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { error } from '../../errors.js';
import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
import {
extract_identifiers,
get_parent,
is_text_attribute,
unwrap_ts_expression
} from '../../utils/ast.js';
import { warn } from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../bindings.js';
Expand Down Expand Up @@ -491,7 +496,7 @@ function validate_call_expression(node, scope, path) {
const rune = get_rune(node, scope);
if (rune === null) return;

const parent = /** @type {import('#compiler').SvelteNode} */ (path.at(-1));
const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1));

if (rune === '$props') {
if (parent.type === 'VariableDeclarator') return;
Expand Down Expand Up @@ -703,7 +708,7 @@ export const validation_runes = merge(validation, a11y_validators, {
next({ ...state });
},
VariableDeclarator(node, { state }) {
const init = node.init;
const init = unwrap_ts_expression(node.init);
const rune = get_rune(init, state.scope);

if (rune === null) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import { create_state_declarators, get_props_method } from '../utils.js';
import { unwrap_ts_expression } from '../../../../utils/ast.js';

/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_runes = {
Expand Down Expand Up @@ -133,7 +134,7 @@ export const javascript_visitors_runes = {
const declarations = [];

for (const declarator of node.declarations) {
const init = declarator.init;
const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active' || rune === '$effect.root') {
if (init != null && is_hoistable_function(init)) {
Expand Down Expand Up @@ -208,7 +209,8 @@ export const javascript_visitors_runes = {
// TODO
continue;
}
const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;

const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const value =
args.length === 0
? b.id('undefined')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { walk } from 'zimmerframe';
import { set_scope, get_rune } from '../../scope.js';
import { extract_identifiers, extract_paths, is_event_attribute } from '../../../utils/ast.js';
import {
extract_identifiers,
extract_paths,
is_event_attribute,
unwrap_ts_expression
} from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
import is_reference from 'is-reference';
import {
Expand Down Expand Up @@ -568,7 +573,8 @@ const javascript_visitors_runes = {
const declarations = [];

for (const declarator of node.declarations) {
const rune = get_rune(declarator.init, state.scope);
const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active') {
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
continue;
Expand All @@ -579,7 +585,7 @@ const javascript_visitors_runes = {
continue;
}

const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const value =
args.length === 0
? b.id('undefined')
Expand Down
39 changes: 39 additions & 0 deletions packages/svelte/src/compiler/utils/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,42 @@ function _extract_paths(assignments = [], param, expression, update_expression)

return assignments;
}

/**
* The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and
* `foo as Bar` as a `TSAsExpression` node. This function unwraps those.
*
* @template {import('#compiler').SvelteNode | undefined | null} T
* @param {T} node
* @returns {T}
*/
export function unwrap_ts_expression(node) {
if (!node) {
return node;
}

// @ts-expect-error these types don't exist on the base estree types
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
// @ts-expect-error
return node.expression;
}

return node;
}

/**
* Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit
* by removing the `| undefined` from the resulting type.
*
* @template {import('#compiler').SvelteNode} T
* @param {T[]} path
* @param {number} at
*/
export function get_parent(path, at) {
let node = path.at(at);
// @ts-expect-error
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
return /** @type {T} */ (path.at(at < 0 ? at - 1 : at + 1));
}
return /** @type {T} */ (node);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
html: '1 2'
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
let count = $state(1) as number;
let double = $derived(count as number * 2) as number;
</script>

{count as number} {double as number}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
html: '1 2'
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
let count = $state(1)!;
let double = $derived(count! * 2)!;
</script>

{count!} {double!}